Skip to content

exceptions: optionally enforce c++ standards #6333

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Aug 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 150 additions & 60 deletions boards.txt

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions cores/esp8266/abi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ extern "C" void __cxa_pure_virtual(void) __attribute__ ((__noreturn__));
extern "C" void __cxa_deleted_virtual(void) __attribute__ ((__noreturn__));


#ifndef __cpp_exceptions
#if !defined(__cpp_exceptions) && !defined(NEW_OOM_ABORT)
void *operator new(size_t size)
{
void *ret = malloc(size);
Expand All @@ -52,7 +52,7 @@ void *operator new[](size_t size)
}
return ret;
}
#endif
#endif // arduino's std::new legacy

void __cxa_pure_virtual(void)
{
Expand Down
33 changes: 32 additions & 1 deletion cores/esp8266/core_esp8266_features.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,37 @@

#define WIFI_HAS_EVENT_CALLBACK

#ifdef __cplusplus

#include <stdlib.h> // malloc()
#include <stddef.h> // size_t

namespace arduino
{
extern "C++"
template <typename T, typename ...TConstructorArgs>
T* new0 (size_t n, TConstructorArgs... TconstructorArgs)
{
// n==0: single allocation, otherwise it is an array
size_t offset = n? sizeof(size_t): 0;
size_t arraysize = n? n: 1;
T* ptr = (T*)malloc(offset + (arraysize * sizeof(T)));
if (ptr)
{
if (n)
*(size_t*)(ptr) = n;
for (size_t i = 0; i < arraysize; i++)
new (ptr + offset + i * sizeof(T)) T(TconstructorArgs...);
return ptr + offset;
}
return nullptr;
}
}

#define arduino_new(Type, ...) arduino::new0<Type>(0, ##__VA_ARGS__)
#define arduino_newarray(Type, n, ...) arduino::new0<Type>(n, ##__VA_ARGS__)

#endif // __cplusplus

#ifndef __STRINGIFY
#define __STRINGIFY(a) #a
Expand Down Expand Up @@ -61,4 +92,4 @@ inline uint32_t esp_get_cycle_count() {
}
#endif // not CORE_MOCK

#endif
#endif // CORE_ESP8266_FEATURES_H
72 changes: 72 additions & 0 deletions doc/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,75 @@ using FPSTR would become...
String response2;
response2 += FPSTR(HTTP);
}

C++
----

- About C++ exceptions, ``operator new``, and Exceptions menu option

The C++ standard says the following about the ``new`` operator behavior when encountering heap shortage (memory full):

- has to throw a ``std::bad_alloc`` C++ exception when they are enabled

- will ``abort()`` otherwise

There are several reasons for the first point above, among which are:

- guarantee that the return of new is never a ``nullptr``

- guarantee full construction of the top level object plus all member subobjects

- guarantee that any subobjects partially constructed get destroyed, and in the correct order, if oom is encountered midway through construction

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.

Historically in Arduino environments, ``new`` is overloaded to simply return the equivalent ``malloc()`` which in turn can return ``nullptr``.

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``).
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.
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.
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.

As of core 2.6.0, there are 3 options: legacy (default) and two clear cases when ``new`` encounters oom:

- ``new`` returns ``nullptr``, with possible bad effects or immediate crash when constructors (called anyway) initialize members (exceptions are disabled in this case)

- C++ exceptions are disabled: ``new`` calls ``abort()`` and will "cleanly" crash, because there is no way to honor memory allocation or to recover gracefully.

- C++ exceptions are enabled: ``new`` throws a ``std::bad_alloc`` C++ exception, which can be caught and handled gracefully.
This assures correct behavior, including handling of all subobjects, which guarantees stability.

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>`__

- New optional allocator ``arduino_new``

A new optional global allocator is introduced with a different semantic:

- never throws exceptions on oom

- never calls constructors on oom

- returns nullptr on oom

It is similar to arduino ``new`` semantic without side effects
(except when parent constructors, or member constructors use ``new``).

Syntax is slightly different, the following shows the different usages:

.. code:: cpp

// with new:

SomeClass* sc = new SomeClass(arg1, arg2, ...);
delete sc;

SomeClass* scs = new SomeClass[42];
delete [] scs;

// with arduino_new:

SomeClass* sc = arduino_new(SomeClass, arg1, arg2, ...);
delete sc;

SomeClass* scs = arduino_newarray(SomeClass, 42);
delete [] scs;
159 changes: 159 additions & 0 deletions libraries/esp8266/examples/arduino_new/arduino_new.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@

// show arduino_new benefits
// released to public domain
// result is below

class SomeClass {
public:
SomeClass(const String& s1 = emptyString, const String& s2 = emptyString) {
Serial.printf("SomeClass@%p(%s)(%s)\n", this, s1.c_str(), s2.c_str());
}

~SomeClass() {
Serial.printf("~ SomeClass @%p\n", this);
}
};

class oom {
private:
char large [65000];
public:
oom() {
Serial.printf("this constructor should not be called\n");
}
};

void setup() {

Serial.begin(115200);
Serial.printf("\n\narduino_new benefits\n\n");
delay(5000); // avoid too frequent bootloop

// arduino_new / arduino_newarray api

Serial.printf("\n----- arduino_new:\n");
auto an = arduino_new(SomeClass);
delete an;

Serial.printf("\n----- arduino_new with oom:\n");
auto anoom = arduino_new(oom);
Serial.printf("nullptr: %p\n", anoom);
delete anoom;

Serial.printf("\n----- arduino_new with constructor parameters:\n");
auto ancp = arduino_new(SomeClass, "param1", "param2");
delete ancp;

Serial.printf("\n----- arduino_newarray[2]\n");
auto ana2 = arduino_newarray(SomeClass, 2);
Serial.printf("@:%p s=%zd s(2)=%zd\n", ana2, sizeof(SomeClass), sizeof(SomeClass[2]));
Serial.printf("0: %p\n", &ana2[0]);
Serial.printf("1: %p\n", &ana2[1]);
delete [] ana2;

Serial.printf("\n----- arduino_newarray[2] (with constructor parameters)\n");
auto ana2cp = arduino_newarray(SomeClass, 2, "param1");
Serial.printf("@:%p s=%zd s(2)=%zd\n", ana2cp, sizeof(SomeClass), sizeof(SomeClass[2]));
Serial.printf("0: %p\n", &ana2cp[0]);
Serial.printf("1: %p\n", &ana2cp[1]);
delete [] ana2cp;

Serial.printf("\n----- arduino_newarray[100000]\n");
auto anaX = arduino_newarray(SomeClass, 100000);
Serial.printf("@:%p\n", anaX);

// standard c++ api for new and new[]

Serial.printf("\n----- new\n");
auto sn = new SomeClass;
delete sn;

Serial.printf("\n----- new with oom: (abort() with option 'Exceptions: Disabled (new can abort)'\n");
auto snoom = new oom;
Serial.printf("nullptr: %p\n", snoom);
delete snoom;

Serial.printf("\n----- new[2]\n");
auto sna2 = new SomeClass[2];
Serial.printf("@:%p s=%zd s(2)=%zd\n", sna2, sizeof(SomeClass), sizeof(SomeClass[2]));
Serial.printf("0: %p\n", &sna2[0]);
Serial.printf("1: %p\n", &sna2[1]);
delete [] sna2;

Serial.printf("\n----- new[10000] (badly fails with 'Exceptions: Legacy' or '...Disabled'\n");
auto snaX = new SomeClass[100000];
Serial.printf("@:%p\n", snaX);
}

void loop() {
}

//////////////////////////////
/*

Result with:
Exceptions: Legacy(new can return nullptr)

//////////////////////////////

arduino_new benefits

----- arduino_new:
SomeClass@0x3fff1864()()
~ SomeClass @0x3fff1864

----- arduino_new with oom:
nullptr: 0

----- arduino_new with constructor parameters:
SomeClass@0x3fff1864(param1)(param2)
~ SomeClass @0x3fff1864

----- arduino_newarray[2]
SomeClass@0x3fff1868()()
SomeClass@0x3fff1869()()
@: 0x3fff1868 s = 1 s(2) = 2
0: 0x3fff1868
1: 0x3fff1869
~ SomeClass @0x3fff1869
~ SomeClass @0x3fff1868

----- arduino_newarray[2](with constructor parameters)
SomeClass@0x3fff1868(param1)()
SomeClass@0x3fff1869(param1)()
@: 0x3fff1868 s = 1 s(2) = 2
0: 0x3fff1868
1: 0x3fff1869
~ SomeClass @0x3fff1869
~ SomeClass @0x3fff1868

----- arduino_newarray[100000]
@: 0

----- new
SomeClass@0x3fff1864()()
~ SomeClass @0x3fff1864

----- new with oom: (abort() with option 'Exceptions: Disabled (new can abort)'
this constructor should not be called
nullptr: 0

----- new[2]
SomeClass@0x3fff1868()()
SomeClass@0x3fff1869()()
@:0x3fff1868 s = 1 s(2) = 2
0: 0x3fff1868
1: 0x3fff1869
~ SomeClass @0x3fff1869
~ SomeClass @0x3fff1868

----- new[10000](badly fails with 'Exceptions: Legacy' or '...Disabled'

Exception(29):
epc1 = 0x402013de epc2 = 0x00000000 epc3 = 0x00000000 excvaddr = 0x00000000 depc = 0x00000000

>>> stack >>>
...

*/
/////////////////////////////
7 changes: 5 additions & 2 deletions tools/boards.txt.py
Original file line number Diff line number Diff line change
Expand Up @@ -883,8 +883,11 @@
]),

'exception_menu': collections.OrderedDict([
( '.menu.exception.disabled', 'Disabled' ),
( '.menu.exception.disabled.build.exception_flags', '-fno-exceptions' ),
( '.menu.exception.legacy', 'Legacy (new can return nullptr)' ),
( '.menu.exception.legacy.build.exception_flags', '-fno-exceptions' ),
( '.menu.exception.legacy.build.stdcpp_lib', '-lstdc++' ),
( '.menu.exception.disabled', 'Disabled (new can abort)' ),
( '.menu.exception.disabled.build.exception_flags', '-fno-exceptions -DNEW_OOM_ABORT' ),
( '.menu.exception.disabled.build.stdcpp_lib', '-lstdc++' ),
( '.menu.exception.enabled', 'Enabled' ),
( '.menu.exception.enabled.build.exception_flags', '-fexceptions' ),
Expand Down