Skip to content

py/modatexit: Add atexit module for deinit functionality. #16063

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

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
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
19 changes: 19 additions & 0 deletions docs/library/atexit.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
:mod:`atexit` -- exit handlers
==============================

.. module:: atexit
:synopsis: The atexit module defines functions to register and unregister cleanup functions.

|see_cpython_module| :mod:`python:atexit`.

Functions
---------

.. function:: register(func)

Register func as a function to be executed on :ref:`soft_reset` or exit on unix/windows ports.
Functions are executed in "lifo" registration order, ie the last registered is the first executed.

.. function:: unregister(func)

Remove func from the list of functions to be run at interpreter shutdown. Not enabled on all ports.
1 change: 1 addition & 0 deletions docs/library/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ library.
:maxdepth: 1

array.rst
atexit.rst
asyncio.rst
binascii.rst
builtins.rst
Expand Down
48 changes: 48 additions & 0 deletions examples/usercmodule/cexample/examplemodule.c
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// Include MicroPython API.
#include "py/runtime.h"
#include "py/modatexit.h"

// Used to get the time in the Timer class example.
#include "py/mphal.h"

static bool custom_library_initialised = false;

// This is the function which will be called from Python as cexample.add_ints(a, b).
static mp_obj_t example_add_ints(mp_obj_t a_obj, mp_obj_t b_obj) {
// Extract the ints from the micropython input objects.
Expand Down Expand Up @@ -149,13 +152,58 @@ MP_DEFINE_CONST_OBJ_TYPE(
locals_dict, &example_Timer_locals_dict
);

// If any data/struct needs to be allocated it should
// be stored in a root pointer to inform the gc not to
// collect it.
// Note this will be reset during a soft-reset.
MP_REGISTER_ROOT_POINTER(uint8_t *example_c_mod_buffer);

MP_DECLARE_CONST_FUN_OBJ_0(example_deinit_obj);

// This __init__ function will be run during module import if
// micropython is built with MICROPY_MODULE_BUILTIN_INIT enabled.
static mp_obj_t example___init__(void) {
if (!custom_library_initialised) {
// __init__ for builtins is called each time the module is imported,
// so ensure that initialisation only happens once.
custom_library_initialised = true;
// this is a good place to create any buffers needed and initialise c libraries or
// hardware peripherals that only need setup once before use by your application.
MP_STATE_VM(example_c_mod_buffer) = m_malloc(8);

#if MICROPY_PY_ATEXIT
mp_atexit_register(MP_OBJ_FROM_PTR(&example_deinit_obj));
#endif
}
return mp_const_none;
}
static MP_DEFINE_CONST_FUN_OBJ_0(example___init___obj, example___init__);

// This will be run during exit / soft-reset (if MICROPY_PY_ATEXIT and
// MICROPY_MODULE_BUILTIN_INIT are enabled) and can be used to reset any
// global static verifies
static mp_obj_t example_deinit(void) {
// This is registered in __init__ be run during exit / soft-reset.
if (custom_library_initialised) {
// global / static are not automatically reset during exit so reset here.
custom_library_initialised = false;
// this can also be used to reset hardware peripherals or c library
// resources such that they're safe to re-initialise in __init__
// again after the soft-reset is complete.
}
return mp_const_none;
}
MP_DEFINE_CONST_FUN_OBJ_0(example_deinit_obj, example_deinit);

// Define all attributes of the module.
// Table entries are key/value pairs of the attribute name (a string)
// and the MicroPython object reference.
// All identifiers and strings are written as MP_QSTR_xxx and will be
// optimized to word-sized integers by the build system (interned strings).
static const mp_rom_map_elem_t example_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_cexample) },
{ MP_ROM_QSTR(MP_QSTR___init__), MP_ROM_PTR(&example___init___obj) },
{ MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&example_deinit_obj) },
{ MP_ROM_QSTR(MP_QSTR_add_ints), MP_ROM_PTR(&example_add_ints_obj) },
{ MP_ROM_QSTR(MP_QSTR_Timer), MP_ROM_PTR(&example_type_Timer) },
{ MP_ROM_QSTR(MP_QSTR_AdvancedTimer), MP_ROM_PTR(&example_type_AdvancedTimer) },
Expand Down
5 changes: 5 additions & 0 deletions ports/qemu/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#include "py/runtime.h"
#include "py/stackctrl.h"
#include "py/gc.h"
#include "py/modatexit.h"
#include "py/mperrno.h"
#include "shared/runtime/gchelper.h"
#include "shared/runtime/pyexec.h"
Expand Down Expand Up @@ -60,6 +61,10 @@ int main(int argc, char **argv) {
}
}

#if MICROPY_PY_ATEXIT
mp_atexit_execute();
#endif

mp_printf(&mp_plat_print, "MPY: soft reboot\n");

gc_sweep_all();
Expand Down
20 changes: 13 additions & 7 deletions ports/unix/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@
#include "py/gc.h"
#include "py/objstr.h"
#include "py/cstack.h"
#include "py/modatexit.h"
#include "py/mperrno.h"
#include "py/mphal.h"
#include "py/mpthread.h"
#include "extmod/misc.h"
#include "extmod/modplatform.h"
#include "extmod/vfs.h"
#include "extmod/vfs_posix.h"
#include "shared/runtime/pyexec.h"
#include "genhdr/mpversion.h"
#include "input.h"

Expand Down Expand Up @@ -87,11 +89,10 @@ static void stderr_print_strn(void *env, const char *str, size_t len) {

const mp_print_t mp_stderr_print = {NULL, stderr_print_strn};

#define FORCED_EXIT (0x100)
// If exc is SystemExit, return value where FORCED_EXIT bit set,
// and lower 8 bits are SystemExit value. For all other exceptions,
// return 1.
static int handle_uncaught_exception(mp_obj_base_t *exc) {
int pyexec_handle_uncaught_exception(mp_obj_base_t *exc) {
// check for SystemExit
if (mp_obj_is_subclass_fast(MP_OBJ_FROM_PTR(exc->type), MP_OBJ_FROM_PTR(&mp_type_SystemExit))) {
// None is an exit value of 0; an int is its value; anything else is 1
Expand All @@ -100,7 +101,7 @@ static int handle_uncaught_exception(mp_obj_base_t *exc) {
if (exit_val != mp_const_none && !mp_obj_get_int_maybe(exit_val, &val)) {
val = 1;
}
return FORCED_EXIT | (val & 255);
return PYEXEC_FORCED_EXIT | (val & 255);
}

// Report all other exceptions
Expand Down Expand Up @@ -171,7 +172,7 @@ static int execute_from_lexer(int source_kind, const void *source, mp_parse_inpu
// uncaught exception
mp_hal_set_interrupt_char(-1);
mp_handle_pending(false);
return handle_uncaught_exception(nlr.ret_val);
return pyexec_handle_uncaught_exception(nlr.ret_val);
}
}

Expand Down Expand Up @@ -271,7 +272,7 @@ static int do_repl(void) {
mp_hal_stdio_mode_orig();

ret = execute_from_lexer(LEX_SRC_VSTR, &line, parse_input_kind, true);
if (ret & FORCED_EXIT) {
if (ret & PYEXEC_FORCED_EXIT) {
return ret;
}
}
Expand Down Expand Up @@ -299,7 +300,7 @@ static int do_repl(void) {

int ret = execute_from_lexer(LEX_SRC_STR, line, MP_PARSE_SINGLE_INPUT, true);
free(line);
if (ret & FORCED_EXIT) {
if (ret & PYEXEC_FORCED_EXIT) {
return ret;
}
}
Expand Down Expand Up @@ -684,7 +685,7 @@ MP_NOINLINE int main_(int argc, char **argv) {
nlr_pop();
} else {
// uncaught exception
return handle_uncaught_exception(nlr.ret_val) & 0xff;
return pyexec_handle_uncaught_exception(nlr.ret_val) & 0xff;
}

// If this module is a package, see if it has a `__main__.py`.
Expand Down Expand Up @@ -756,6 +757,11 @@ MP_NOINLINE int main_(int argc, char **argv) {
}
}

#if MICROPY_PY_ATEXIT
int atexit_code = mp_atexit_execute();
ret = (atexit_code != 0) ? atexit_code : ret;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CPython doesn't seem to update the process exit code based on anything which happens in an atexit handler (I checked by first reading the source and then writing a silly example):

import atexit

def ohno():
    raise SystemExit(99)

atexit.register(ohno)

raise RuntimeError("fail")
❯ python atexit.py
Traceback (most recent call last):
  File "/home/gus/ry/george/tmp/atexit.py", line 8, in <module>
    raise RuntimeError("fail")
RuntimeError: fail
Exception ignored in atexit callback <function ohno at 0x7e1cfef93600>:
Traceback (most recent call last):
  File "/home/gus/ry/george/tmp/atexit.py", line 4, in ohno
    raise SystemExit(99)
SystemExit: 99
❯ echo $status
1

(Similarly if you comment the raise RuntimeError() line, the exit code becomes 0.)

Removing some of the machinery for this might(?) save a small amount of code size on bare metal ports as well, if we're lucky...?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's an interesting point, yeah I assumed we should capture any failures here but for code size and compatibility it's worth trying without

#endif

#if MICROPY_PY_SYS_SETTRACE
MP_STATE_THREAD(prof_trace_callback) = MP_OBJ_NULL;
#endif
Expand Down
1 change: 1 addition & 0 deletions ports/webassembly/mpconfigport.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
#define MICROPY_FLOAT_IMPL (MICROPY_FLOAT_IMPL_DOUBLE)
#define MICROPY_USE_INTERNAL_ERRNO (1)
#define MICROPY_USE_INTERNAL_PRINTF (0)
#define MICROPY_PY_ATEXIT (0)

#define MICROPY_EPOCH_IS_1970 (1)
#define MICROPY_PY_ASYNCIO_TASK_QUEUE_PUSH_CALLBACK (1)
Expand Down
101 changes: 101 additions & 0 deletions py/modatexit.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* This file is part of the MicroPython project, http://micropython.org/
*
* The MIT License (MIT)
*
* Copyright (c) 2025 Andrew Leech
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

#include <assert.h>
#include <string.h>

#include "py/obj.h"
#include "py/runtime.h"
#include "py/modatexit.h"
#include "shared/runtime/pyexec.h"

#if MICROPY_PY_ATEXIT

mp_obj_t mp_atexit_register(mp_obj_t function) {
if (!mp_obj_is_callable(function)) {
mp_raise_ValueError(MP_ERROR_TEXT("function not callable"));

Check warning on line 39 in py/modatexit.c

View check run for this annotation

Codecov / codecov/patch

py/modatexit.c#L39

Added line #L39 was not covered by tests
}
if (MP_STATE_VM(atexit_handlers) == NULL) {
MP_STATE_VM(atexit_handlers) = MP_OBJ_TO_PTR(mp_obj_new_list(0, NULL));
}
mp_obj_list_append(MP_OBJ_FROM_PTR(MP_STATE_VM(atexit_handlers)), function);
// return the passed in function so this can be used as a decorator
return function;
}
static MP_DEFINE_CONST_FUN_OBJ_1(mp_atexit_register_obj, mp_atexit_register);

#if MICROPY_PY_ATEXIT_UNREGISTER
mp_obj_t mp_atexit_unregister(mp_obj_t function) {
nlr_buf_t nlr;
// ValueError is thrown when function is no longer in the list
if (nlr_push(&nlr) == 0) {
while (MP_STATE_VM(atexit_handlers) != NULL) {
mp_obj_list_remove(MP_OBJ_FROM_PTR(MP_STATE_VM(atexit_handlers)), function);
}
}
return mp_const_none;
}
static MP_DEFINE_CONST_FUN_OBJ_1(mp_atexit_unregister_obj, mp_atexit_unregister);
#endif

// port specific shutdown procedures should cLl this
// to run any registered atexit handlers.
int mp_atexit_execute(void) {
int exit_code = 0;
if (MP_STATE_VM(atexit_handlers) != NULL) {
mp_obj_list_t *list = MP_STATE_VM(atexit_handlers);
for (int i = list->len - 1; i >= 0; i--) {
mp_obj_t function = list->items[i];

nlr_buf_t nlr;
if (nlr_push(&nlr) == 0) {
mp_call_function_0(function);
} else {
exit_code = pyexec_handle_uncaught_exception(nlr.ret_val);

Check warning on line 77 in py/modatexit.c

View check run for this annotation

Codecov / codecov/patch

py/modatexit.c#L77

Added line #L77 was not covered by tests
}
}
}
return exit_code;
}

static const mp_rom_map_elem_t mp_module_atexit_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_atexit) },
{ MP_ROM_QSTR(MP_QSTR_register), MP_ROM_PTR(&mp_atexit_register_obj) },
#if MICROPY_PY_ATEXIT_UNREGISTER
{ MP_ROM_QSTR(MP_QSTR_unregister), MP_ROM_PTR(&mp_atexit_unregister_obj) },
#endif
};
static MP_DEFINE_CONST_DICT(mp_module_atexit_globals, mp_module_atexit_globals_table);

const mp_obj_module_t mp_module_atexit = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t *)&mp_module_atexit_globals,
};

MP_REGISTER_ROOT_POINTER(mp_obj_list_t * atexit_handlers);
MP_REGISTER_MODULE(MP_QSTR_atexit, mp_module_atexit);

#endif // MICROPY_PY_ATEXIT
33 changes: 33 additions & 0 deletions py/modatexit.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* This file is part of the MicroPython project, http://micropython.org/
*
* The MIT License (MIT)
*
* Copyright (c) 2024 Andrew Leech
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#ifndef MICROPY_INCLUDED_PY_MODATEXIT_H
#define MICROPY_INCLUDED_PY_MODATEXIT_H

int mp_atexit_execute(void);
mp_obj_t mp_atexit_register(mp_obj_t callback);
mp_obj_t mp_atexit_unregister(mp_obj_t callback);

#endif // MICROPY_INCLUDED_PY_MODATEXIT_H
8 changes: 8 additions & 0 deletions py/mpconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -1351,6 +1351,14 @@ typedef double mp_float_t;
#define MICROPY_PY_ARRAY_SLICE_ASSIGN (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
#endif

// Whether to provide "atexit" module.
#ifndef MICROPY_PY_ATEXIT
#define MICROPY_PY_ATEXIT (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
#endif
#ifndef MICROPY_PY_ATEXIT_UNREGISTER
#define MICROPY_PY_ATEXIT_UNREGISTER (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING)
#endif

// Whether to support attrtuple type (MicroPython extension)
// It provides space-efficient tuples with attribute access
#ifndef MICROPY_PY_ATTRTUPLE
Expand Down
1 change: 1 addition & 0 deletions py/py.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ set(MICROPY_SOURCE_PY
${MICROPY_PY_DIR}/malloc.c
${MICROPY_PY_DIR}/map.c
${MICROPY_PY_DIR}/modarray.c
${MICROPY_PY_DIR}/modatexit.c
${MICROPY_PY_DIR}/modbuiltins.c
${MICROPY_PY_DIR}/modcmath.c
${MICROPY_PY_DIR}/modcollections.c
Expand Down
1 change: 1 addition & 0 deletions py/py.mk
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ PY_CORE_O_BASENAME = $(addprefix py/,\
builtinevex.o \
builtinhelp.o \
modarray.o \
modatexit.o \
modbuiltins.o \
modcollections.o \
modgc.o \
Expand Down
4 changes: 4 additions & 0 deletions py/runtime.c
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ void mp_init(void) {
MP_STATE_VM(sys_mutable[MP_SYS_MUTABLE_TRACEBACKLIMIT]) = MP_OBJ_NEW_SMALL_INT(1000);
#endif

#if MICROPY_PY_ATEXIT
MP_STATE_VM(atexit_handlers) = NULL;
#endif

#if MICROPY_PY_BLUETOOTH
MP_STATE_VM(bluetooth) = MP_OBJ_NULL;
#endif
Expand Down
Loading
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy