-
-
Notifications
You must be signed in to change notification settings - Fork 8.2k
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
base: master
Are you sure you want to change the base?
Conversation
Code size report:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sounds more like atexit()
than __del__
. Could we implement it that way instead?
Once nice thing about atexit()
is that the call order is deterministic, which could be important in some cases.
py/objmodule.c
Outdated
@@ -170,6 +170,21 @@ static const mp_module_delegation_entry_t mp_builtin_module_delegation_table[] = | |||
}; | |||
#endif | |||
|
|||
#if MICROPY_MODULE_BUILTIN_INIT | |||
void mp_module_builtin_deinit() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
void mp_module_builtin_deinit() { | |
void mp_module_builtin_deinit(void) { |
py/objmodule.c
Outdated
mp_obj_t dest[2]; | ||
mp_load_method_maybe(mp_builtin_module_map.table[i].value, MP_QSTR___del__, dest); | ||
if (dest[0] != MP_OBJ_NULL) { | ||
mp_call_method_n_kw(0, 0, dest); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if this raises an exception?
27f2d94
to
3fb1c8b
Compare
Yeah as I was initially writing up the trade-offs/alternatives section above I was thinking an atexit approach probably would be much better. I've been thinking about it more, my feeling is it would be quite low overhead to simply extend the existing system.atexit function to store all provided functions in a linked list, rather than being the single slot that gets replaced currently. Then enable that by rom level and integrate into all ports. If anyone needs it as a separate atexit module for compatibility that could be done as a wrapper in micropython-lib I'm not sure that there's really any great need to also implement the remove function, just registering functions is probably enough to start with |
+1 it's more standard and easier to discover. |
3fb1c8b
to
fb36eab
Compare
py/modsys.c
Outdated
struct _m_atexit_node_t *next; | ||
mp_obj_t fn; | ||
} m_atexit_node_t; | ||
|
||
// atexit(callback): Callback is called when sys.exit is called. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you update the comment that this is LIFO ?
py/modsys.c
Outdated
if (nlr_push(&nlr) == 0) { | ||
mp_call_function_0(MP_STATE_VM(sys_exitfunc)->fn); | ||
} else { | ||
// Uncaught exception: print it out. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In CPython a SystemExit would still cause an actual exit, is it worth adding that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are currently only being run during an exit / soft-restart so I wouldn't think the handling should change?
Though maybe the exception shouldn't be printed out if it's a SystemExit
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so I wouldn't think the handling should change?
It is actually changing: for the unix port an exception called during the atexit callback would result in exit code 1 (because of an 'FATAL: uncaught NLR', maybe not ideal, but at least it's a non-zero exit code and that is what matters). But with this implementation the exception would be swallowed (and printed). That should not change imo and this should remain CPython-compatible in the sense that an exception during atexit is treated as an exception. Note CPython does this:
If an exception is raised during execution of the exit handlers, a traceback is printed (unless SystemExit is raised) and the exception information is saved. After all exit handlers have had a chance to run, the last exception to be raised is re-raised.
But given your question, for other ports I assume raising an exception here would actually change behavior?
Though maybe the exception shouldn't be printed out if it's a SystemExit
That's indeed what CPython does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, that detail helps thanks! The filter on whether to print the exception is easy enough to implement certainly. Maybe then just overriding the exit code in the exception handler here is enough to broadly match cpython, as opposed to keeping a copy of the exception details to pass them onto any default handler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe then just overriding the exit code in the exception handler here is enough to broadly match cpython
Probably, but in the end it would be nicer to fix this properly:
as opposed to keeping a copy of the exception details to pass them onto any default handler.
Ideally the only thing which would be needed is to store nlr.ret_val
then if not NULL raise it again after having called all handlers. Should not be an issue code-size-wise. Passing it onto the default handler would then happen automatically because the main code knows how to do that; however, this requires that mp_sys_atexit_execute
is being called within execute_from_lexer
(or whatever the counterpart is named for the various ports).
This is not the case for the unix port now, nor for the other ports. I don't know why though, and all in all this is maybe not very logical/expected; e.g. for the stm32 port it runs after deinitializing pretty much all of the hardware: is there a specific reason you did it that way? Would it not be more sound if there's not many restrictions to what can run in the atexit handler i.e. can still use hardware if it chooses to etc?
Also if this atexit handler runs within the normal execution phase, you can probably use an normal mp_obj_list_t
or similar instead of manually implementing a linked list, which should also reduce code size?
ports/unix/main.c
Outdated
@@ -753,10 +754,7 @@ MP_NOINLINE int main_(int argc, char **argv) { | |||
#endif | |||
|
|||
#if MICROPY_PY_SYS_ATEXIT | |||
// Beware, the sys.settrace callback should be disabled before running sys.atexit. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment was added in 310b3d1
Does anyone know why this would be the case and if it's still relevant?
fb36eab
to
53e0f3a
Compare
I'm going to try reworking to the a cpython compatibly |
6404d46
to
0bd99fd
Compare
066a498
to
22e87fb
Compare
477a563
to
2415ef8
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #16063 +/- ##
==========================================
- Coverage 98.54% 98.53% -0.01%
==========================================
Files 169 170 +1
Lines 21877 21902 +25
==========================================
+ Hits 21558 21581 +23
- Misses 319 321 +2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
2415ef8
to
bffbecc
Compare
Try
|
Signed-off-by: Andrew Leech <andrew@alelec.net>
Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
Also improve consistency with the matching functionality in the unix port. Signed-off-by: Andrew Leech <andrew@alelec.net>
Signed-off-by: Andrew Leech <andrew@alelec.net>
Signed-off-by: Andrew Leech <andrew@alelec.net>
Signed-off-by: Andrew Leech <andrew@alelec.net>
There is no common deinit routine to add support in. Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
Signed-off-by: Andrew Leech <andrew@alelec.net>
bffbecc
to
4bd2fa2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This implementation looks generally good to me, nice and clean. And potentially very useful!
I have a couple of inline questions. Also I'm a little surprised at the amount of code size impact on some ports given how little code seems to be compiled in (assuming register-only). But maybe that's actually inline with expectations.
@@ -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; |
There was a problem hiding this comment.
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...?
There was a problem hiding this comment.
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
@@ -175,6 +189,11 @@ static int parse_compile_execute(const void *source, mp_parse_input_kind_t input | |||
mp_hal_stdout_tx_strn("\x04", 1); | |||
} | |||
|
|||
#if MICROPY_PY_ATEXIT | |||
int atexit_code = mp_atexit_execute(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't tested this, but placing this here in parse_compile_execute()
seems like it might have some unexpected consequences? For example, if you put an atexit.register()
call in boot.py
then will the handler be run after executing boot.py
and before executing main.py
?
(I think this draws attention to a shortcoming in the current architecture, which is that AFAIK there's no non-hardware-specific soft reset hook function that all ports can call for things which need cleanup in the soft reset path.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I've realised similar issues with this location and been testing / looking at other ways to add this cleanly.
I've actually been fighting the urge to undertake a broad refactor & consolidation of soft-reset / shutdown handling in ports to create the "correct" place to run the handler code! I'd rather not block this change behind such a large refactor though.
Perhaps for now I should go back to my earlier plan to just add the execute call to an appropriate place in each main.c
Then they'll all get cleaned up anyway if I get to the refactor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps for now I should go back to my earlier plan to just add the execute call to an appropriate place in each main.c Then they'll all get cleaned up anyway if I get to the refactor.
That sounds like a good approach to me. FWIW, I have also felt this urge... but better to do it as a pure refactor, for sure.
I think this will be similar to my recent PR exposing RingIO, adding python interfaces does seem to be surprisingly large in many cases. |
@projectgus / @dpgeorge I'm also running into an issue with trying to run the unit test on qemu port, which also applies to running it on any live hardware that connects with pyboard.py behind the scenes; Basically pyboard.py uses the raw mode and paste to drop the test code onto the target and runs it that way, capturing just the output from the code run in this raw mode session. I recently added the Is there any other tests I can reference that connect over a soft-restart / keeps the raw connection open to capture all the output during this time? |
Summary
Edit: I pivoted this MR to instead implement atexit instead. Maybe it should have been a new PR.... Regardless I'll update this with more details later.
Orig:
This PR proposes a pattern for adding a deinit function to any builtin module (including user c modules).
Similar to what I was trying to achieve in #13011 I've run into an issue safely developing a user c module such that it works correctly over soft-reset. In particular, this module needs to know when a soft-reset occurs so that it can re-initialise its global / MP_STATE_VM variables.
These builtin modules already have the ability to register an
__init__
function which gets run on import. This PR mirrors this with a__del__
function which (if defined) will be run during soft reset.I'm also inclined to think this mechanism could be used for a range of other builtin c modules which currently need to be manually listed in each ports main.c to deinit as needed. Using this automatic method could ease maintenance of ther ports.
Similarly, there's a lot of duplication in the soft-reset loop of most ports already; is there a reason the
gc_sweep_all();
andmp_module_builtin_deinit();
shouldn't be included in themp_deinit()
function?Lastly, should this mechanism possibly be extended to include all modules
MP_STATE_VM(mp_loaded_modules_dict)
to allow any module (inc python and dynamic c) to define a__del__
function that's run on soft-reset?Testing
I've been testing this with my work to make lvgl work cleanly as a user c module. If this mechanism seems reasonable, I'll also add this functionality to the c example module and try to figure some way to add a test to the coverage tests,
Trade-offs and Alternatives
I initially tried to use the existing gc finaliser mechanisms demonstrated in #13011 however, similar to in the PR, I found it quite non-deterministic. It also required the use of a C class registered as a singleton which the module didn't otherwise have or need. I failed to find a way that ensured this singleton class would be actually deleted with the del function run reliable during the soft restart, it just would not run for me.
I added this as a matching function to the existing builtin init functionality, hoewver looking at cpython the atexit module is a more standard way to handle this kind of thing. I see there's already a sys.atexit micropython extension but it's only a single global function and it's only implemented on the windows port. Adding full atexit functionality that works with a linked list or similar would be more compatible, but would take more flash (to add a module / extend sys.atexit) and ram (holding the linked list) and run-time (to register the atexit functions).