Skip to content

Commit 85f1ea7

Browse files
authored
exceptions: optionally enforce c++ standards (#6333)
* exceptions: 3 choices: legacy, std::new never returns 0, or exceptions enabled * arduino_new (doc, example, array)
1 parent 0a031ce commit 85f1ea7

File tree

6 files changed

+420
-65
lines changed

6 files changed

+420
-65
lines changed

boards.txt

+150-60
Large diffs are not rendered by default.

cores/esp8266/abi.cpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ extern "C" void __cxa_pure_virtual(void) __attribute__ ((__noreturn__));
3232
extern "C" void __cxa_deleted_virtual(void) __attribute__ ((__noreturn__));
3333

3434

35-
#ifndef __cpp_exceptions
35+
#if !defined(__cpp_exceptions) && !defined(NEW_OOM_ABORT)
3636
void *operator new(size_t size)
3737
{
3838
void *ret = malloc(size);
@@ -52,7 +52,7 @@ void *operator new[](size_t size)
5252
}
5353
return ret;
5454
}
55-
#endif
55+
#endif // arduino's std::new legacy
5656

5757
void __cxa_pure_virtual(void)
5858
{

cores/esp8266/core_esp8266_features.h

+32-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,37 @@
3232

3333
#define WIFI_HAS_EVENT_CALLBACK
3434

35+
#ifdef __cplusplus
36+
37+
#include <stdlib.h> // malloc()
38+
#include <stddef.h> // size_t
39+
40+
namespace arduino
41+
{
42+
extern "C++"
43+
template <typename T, typename ...TConstructorArgs>
44+
T* new0 (size_t n, TConstructorArgs... TconstructorArgs)
45+
{
46+
// n==0: single allocation, otherwise it is an array
47+
size_t offset = n? sizeof(size_t): 0;
48+
size_t arraysize = n? n: 1;
49+
T* ptr = (T*)malloc(offset + (arraysize * sizeof(T)));
50+
if (ptr)
51+
{
52+
if (n)
53+
*(size_t*)(ptr) = n;
54+
for (size_t i = 0; i < arraysize; i++)
55+
new (ptr + offset + i * sizeof(T)) T(TconstructorArgs...);
56+
return ptr + offset;
57+
}
58+
return nullptr;
59+
}
60+
}
61+
62+
#define arduino_new(Type, ...) arduino::new0<Type>(0, ##__VA_ARGS__)
63+
#define arduino_newarray(Type, n, ...) arduino::new0<Type>(n, ##__VA_ARGS__)
64+
65+
#endif // __cplusplus
3566

3667
#ifndef __STRINGIFY
3768
#define __STRINGIFY(a) #a
@@ -61,4 +92,4 @@ inline uint32_t esp_get_cycle_count() {
6192
}
6293
#endif // not CORE_MOCK
6394

64-
#endif
95+
#endif // CORE_ESP8266_FEATURES_H

doc/reference.rst

+72
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,75 @@ using FPSTR would become...
215215
String response2;
216216
response2 += FPSTR(HTTP);
217217
}
218+
219+
C++
220+
----
221+
222+
- About C++ exceptions, ``operator new``, and Exceptions menu option
223+
224+
The C++ standard says the following about the ``new`` operator behavior when encountering heap shortage (memory full):
225+
226+
- has to throw a ``std::bad_alloc`` C++ exception when they are enabled
227+
228+
- will ``abort()`` otherwise
229+
230+
There are several reasons for the first point above, among which are:
231+
232+
- guarantee that the return of new is never a ``nullptr``
233+
234+
- guarantee full construction of the top level object plus all member subobjects
235+
236+
- guarantee that any subobjects partially constructed get destroyed, and in the correct order, if oom is encountered midway through construction
237+
238+
When C++ exceptions are disabled, or when using ``new(nothrow)``, the above guarantees can't be upheld, so the second point (``abort()``) above is the only ``std::c++`` viable solution.
239+
240+
Historically in Arduino environments, ``new`` is overloaded to simply return the equivalent ``malloc()`` which in turn can return ``nullptr``.
241+
242+
This behavior is not C++ standard, and there is good reason for that: there are hidden and very bad side effects. The *class and member constructors are always called, even when memory is full* (``this == nullptr``).
243+
In addition, the memory allocation for the top object could succeed, but allocation required for some member object could fail, leaving construction in an undefined state.
244+
So the historical behavior of Ardudino's ``new``, when faced with insufficient memory, will lead to bad crashes sooner or later, sometimes unexplainable, generally due to memory corruption even when the returned value is checked and managed.
245+
Luckily on esp8266, trying to update RAM near address 0 will immediately raise an hardware exception, unlike on other uC like avr on which that memory can be accessible.
246+
247+
As of core 2.6.0, there are 3 options: legacy (default) and two clear cases when ``new`` encounters oom:
248+
249+
- ``new`` returns ``nullptr``, with possible bad effects or immediate crash when constructors (called anyway) initialize members (exceptions are disabled in this case)
250+
251+
- C++ exceptions are disabled: ``new`` calls ``abort()`` and will "cleanly" crash, because there is no way to honor memory allocation or to recover gracefully.
252+
253+
- C++ exceptions are enabled: ``new`` throws a ``std::bad_alloc`` C++ exception, which can be caught and handled gracefully.
254+
This assures correct behavior, including handling of all subobjects, which guarantees stability.
255+
256+
History: `#6269 <https://github.com/esp8266/Arduino/issues/6269>`__ `#6309 <https://github.com/esp8266/Arduino/pull/6309>`__ `#6312 <https://github.com/esp8266/Arduino/pull/6312>`__
257+
258+
- New optional allocator ``arduino_new``
259+
260+
A new optional global allocator is introduced with a different semantic:
261+
262+
- never throws exceptions on oom
263+
264+
- never calls constructors on oom
265+
266+
- returns nullptr on oom
267+
268+
It is similar to arduino ``new`` semantic without side effects
269+
(except when parent constructors, or member constructors use ``new``).
270+
271+
Syntax is slightly different, the following shows the different usages:
272+
273+
.. code:: cpp
274+
275+
// with new:
276+
277+
SomeClass* sc = new SomeClass(arg1, arg2, ...);
278+
delete sc;
279+
280+
SomeClass* scs = new SomeClass[42];
281+
delete [] scs;
282+
283+
// with arduino_new:
284+
285+
SomeClass* sc = arduino_new(SomeClass, arg1, arg2, ...);
286+
delete sc;
287+
288+
SomeClass* scs = arduino_newarray(SomeClass, 42);
289+
delete [] scs;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
2+
// show arduino_new benefits
3+
// released to public domain
4+
// result is below
5+
6+
class SomeClass {
7+
public:
8+
SomeClass(const String& s1 = emptyString, const String& s2 = emptyString) {
9+
Serial.printf("SomeClass@%p(%s)(%s)\n", this, s1.c_str(), s2.c_str());
10+
}
11+
12+
~SomeClass() {
13+
Serial.printf("~ SomeClass @%p\n", this);
14+
}
15+
};
16+
17+
class oom {
18+
private:
19+
char large [65000];
20+
public:
21+
oom() {
22+
Serial.printf("this constructor should not be called\n");
23+
}
24+
};
25+
26+
void setup() {
27+
28+
Serial.begin(115200);
29+
Serial.printf("\n\narduino_new benefits\n\n");
30+
delay(5000); // avoid too frequent bootloop
31+
32+
// arduino_new / arduino_newarray api
33+
34+
Serial.printf("\n----- arduino_new:\n");
35+
auto an = arduino_new(SomeClass);
36+
delete an;
37+
38+
Serial.printf("\n----- arduino_new with oom:\n");
39+
auto anoom = arduino_new(oom);
40+
Serial.printf("nullptr: %p\n", anoom);
41+
delete anoom;
42+
43+
Serial.printf("\n----- arduino_new with constructor parameters:\n");
44+
auto ancp = arduino_new(SomeClass, "param1", "param2");
45+
delete ancp;
46+
47+
Serial.printf("\n----- arduino_newarray[2]\n");
48+
auto ana2 = arduino_newarray(SomeClass, 2);
49+
Serial.printf("@:%p s=%zd s(2)=%zd\n", ana2, sizeof(SomeClass), sizeof(SomeClass[2]));
50+
Serial.printf("0: %p\n", &ana2[0]);
51+
Serial.printf("1: %p\n", &ana2[1]);
52+
delete [] ana2;
53+
54+
Serial.printf("\n----- arduino_newarray[2] (with constructor parameters)\n");
55+
auto ana2cp = arduino_newarray(SomeClass, 2, "param1");
56+
Serial.printf("@:%p s=%zd s(2)=%zd\n", ana2cp, sizeof(SomeClass), sizeof(SomeClass[2]));
57+
Serial.printf("0: %p\n", &ana2cp[0]);
58+
Serial.printf("1: %p\n", &ana2cp[1]);
59+
delete [] ana2cp;
60+
61+
Serial.printf("\n----- arduino_newarray[100000]\n");
62+
auto anaX = arduino_newarray(SomeClass, 100000);
63+
Serial.printf("@:%p\n", anaX);
64+
65+
// standard c++ api for new and new[]
66+
67+
Serial.printf("\n----- new\n");
68+
auto sn = new SomeClass;
69+
delete sn;
70+
71+
Serial.printf("\n----- new with oom: (abort() with option 'Exceptions: Disabled (new can abort)'\n");
72+
auto snoom = new oom;
73+
Serial.printf("nullptr: %p\n", snoom);
74+
delete snoom;
75+
76+
Serial.printf("\n----- new[2]\n");
77+
auto sna2 = new SomeClass[2];
78+
Serial.printf("@:%p s=%zd s(2)=%zd\n", sna2, sizeof(SomeClass), sizeof(SomeClass[2]));
79+
Serial.printf("0: %p\n", &sna2[0]);
80+
Serial.printf("1: %p\n", &sna2[1]);
81+
delete [] sna2;
82+
83+
Serial.printf("\n----- new[10000] (badly fails with 'Exceptions: Legacy' or '...Disabled'\n");
84+
auto snaX = new SomeClass[100000];
85+
Serial.printf("@:%p\n", snaX);
86+
}
87+
88+
void loop() {
89+
}
90+
91+
//////////////////////////////
92+
/*
93+
94+
Result with:
95+
Exceptions: Legacy(new can return nullptr)
96+
97+
//////////////////////////////
98+
99+
arduino_new benefits
100+
101+
----- arduino_new:
102+
SomeClass@0x3fff1864()()
103+
~ SomeClass @0x3fff1864
104+
105+
----- arduino_new with oom:
106+
nullptr: 0
107+
108+
----- arduino_new with constructor parameters:
109+
SomeClass@0x3fff1864(param1)(param2)
110+
~ SomeClass @0x3fff1864
111+
112+
----- arduino_newarray[2]
113+
SomeClass@0x3fff1868()()
114+
SomeClass@0x3fff1869()()
115+
@: 0x3fff1868 s = 1 s(2) = 2
116+
0: 0x3fff1868
117+
1: 0x3fff1869
118+
~ SomeClass @0x3fff1869
119+
~ SomeClass @0x3fff1868
120+
121+
----- arduino_newarray[2](with constructor parameters)
122+
SomeClass@0x3fff1868(param1)()
123+
SomeClass@0x3fff1869(param1)()
124+
@: 0x3fff1868 s = 1 s(2) = 2
125+
0: 0x3fff1868
126+
1: 0x3fff1869
127+
~ SomeClass @0x3fff1869
128+
~ SomeClass @0x3fff1868
129+
130+
----- arduino_newarray[100000]
131+
@: 0
132+
133+
----- new
134+
SomeClass@0x3fff1864()()
135+
~ SomeClass @0x3fff1864
136+
137+
----- new with oom: (abort() with option 'Exceptions: Disabled (new can abort)'
138+
this constructor should not be called
139+
nullptr: 0
140+
141+
----- new[2]
142+
SomeClass@0x3fff1868()()
143+
SomeClass@0x3fff1869()()
144+
@:0x3fff1868 s = 1 s(2) = 2
145+
0: 0x3fff1868
146+
1: 0x3fff1869
147+
~ SomeClass @0x3fff1869
148+
~ SomeClass @0x3fff1868
149+
150+
----- new[10000](badly fails with 'Exceptions: Legacy' or '...Disabled'
151+
152+
Exception(29):
153+
epc1 = 0x402013de epc2 = 0x00000000 epc3 = 0x00000000 excvaddr = 0x00000000 depc = 0x00000000
154+
155+
>>> stack >>>
156+
...
157+
158+
*/
159+
/////////////////////////////

tools/boards.txt.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -883,8 +883,11 @@
883883
]),
884884

885885
'exception_menu': collections.OrderedDict([
886-
( '.menu.exception.disabled', 'Disabled' ),
887-
( '.menu.exception.disabled.build.exception_flags', '-fno-exceptions' ),
886+
( '.menu.exception.legacy', 'Legacy (new can return nullptr)' ),
887+
( '.menu.exception.legacy.build.exception_flags', '-fno-exceptions' ),
888+
( '.menu.exception.legacy.build.stdcpp_lib', '-lstdc++' ),
889+
( '.menu.exception.disabled', 'Disabled (new can abort)' ),
890+
( '.menu.exception.disabled.build.exception_flags', '-fno-exceptions -DNEW_OOM_ABORT' ),
888891
( '.menu.exception.disabled.build.stdcpp_lib', '-lstdc++' ),
889892
( '.menu.exception.enabled', 'Enabled' ),
890893
( '.menu.exception.enabled.build.exception_flags', '-fexceptions' ),

0 commit comments

Comments
 (0)