Skip to content

py/builtinimport: Allow overriding of mp_builtin___import__. #9026

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

Closed

Conversation

laurensvalk
Copy link
Contributor

This allows MicroPython ports without external imports enabled to override mp_builtin___import__.

This is useful for MicroPython applications without enough resources to have MICROPY_ENABLE_EXTERNAL_IMPORT enabled, but which still want to allow users to do some application-specific imports beyond builtins.

Ports can already provide things like mp_builtin_open, so providing mp_builtin___import__ seems to fit reasonably well.

Instead of doing it this way, it could be made more generic by changing MICROPY_ENABLE_EXTERNAL_IMPORT to a configuration setting MICROPY_ENABLE_IMPORT with three options, such as: MICROPY_ENABLE_IMPORT_EXTERNAL, MICROPY_ENABLE_IMPORT_BUILTIN_ONLY, and MICROPY_ENABLE_IMPORT_CUSTOM.


Context: We are currently overriding it in MicroPython for Pybricks. I was considering adding certain new functionality (inspired by #8381), but overriding it locally without forking would be a bit cleaner and easier to maintain.

@codecov-commenter
Copy link

Codecov Report

Merging #9026 (c0ac815) into master (9dfabcd) will not change coverage.
The diff coverage is n/a.

@@           Coverage Diff           @@
##           master    #9026   +/-   ##
=======================================
  Coverage   98.37%   98.37%           
=======================================
  Files         156      156           
  Lines       20281    20281           
=======================================
  Hits        19951    19951           
  Misses        330      330           
Impacted Files Coverage Δ
py/builtinimport.c 98.92% <ø> (ø)

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

@dpgeorge
Copy link
Member

dpgeorge commented Aug 8, 2022

This is useful for MicroPython applications without enough resources to have MICROPY_ENABLE_EXTERNAL_IMPORT enabled, but which still want to allow users to do some application-specific imports beyond builtins.

If a port wants to provide "custom" imports the idea is for it to enable MICROPY_ENABLE_EXTERNAL_IMPORT and then provide the desired functionality via a custom mp_import_stat(). It doesn't need to have a filesystem.

But it looks like you actually want more sophisticated import behaviour, to support built-in packages. #5701 intended to do this but was closed. Maybe it needs to be revisited? Or even the patch you link above could be merged here?

I'm not against being able to override mp_builtin___import__(), if it makes sense. But I think it should be done a bit differently to match mp_builtin_open() in py/builtin.h.

@laurensvalk
Copy link
Contributor Author

laurensvalk commented Aug 8, 2022

If a port wants to provide "custom" imports the idea is for it to enable MICROPY_ENABLE_EXTERNAL_IMPORT and then provide the desired functionality via a custom mp_import_stat(). It doesn't need to have a filesystem.

Our main resource constraint is build size. On the minimal port, MICROPY_ENABLE_EXTERNAL_IMPORT requires about 1372 bytes. We don't use most of it, so it is relatively expensive to add customizations that way (≈2%).

But it looks like you actually want more sophisticated import behaviour, to support built-in packages.

The current PR is motivated by adding another (future) customization inspired by #8381, so that's why I started looking for a more generic way to customize imports. If adding that gets us closer in terms of build size, maybe we'll just enable external imports though.

#5701 intended to [support built-in packages] but was closed. Maybe it needs to be revisited? Or even the patch you link above could be merged here?

The patch we used served us well over the past years but I wasn't sure if it was generic enough to be useful in MicroPython. If you think it is, I'd be happy to make a pull request. This particular patch might not actually be needed once we drop external imports from all of our ports (it's still enabled on some) and just register a module with a dot in the name.

I'm not against being able to override mp_builtin___import__(), if it makes sense. But I think it should be done a bit differently to match mp_builtin_open() in py/builtin.h.

Thanks, I'll have a look at how that works.

Another option would be to add a hook that gets called just prior to raising the ImportError. Ports can then fill in the gap as needed. It's probably only useful if MICROPY_ENABLE_EXTERNAL_IMPORT is disabled but it could be added in both places in principle.

Like this, for example:

diff --git a/py/builtin.h b/py/builtin.h
index a6f824ca2..398470bef 100644
--- a/py/builtin.h
+++ b/py/builtin.h
@@ -65,6 +65,13 @@ MP_DECLARE_CONST_FUN_OBJ_KW(mp_builtin_open_obj);
 #endif
 
 mp_obj_t mp_builtin___import__(size_t n_args, const mp_obj_t *args);
+
+// A port can extend mp_builtin___import__ to handle cases where builtin import
+// can't find the requested module.
+#if MICROPY_MODULE_BUILTIN_IMPORT_EXTRA
+mp_obj_t mp_builtin_import_extra(size_t n_args, const mp_obj_t *args);
+#endif
+
 mp_obj_t mp_micropython_mem_info(size_t n_args, const mp_obj_t *args);
 
 MP_DECLARE_CONST_FUN_OBJ_VAR(mp_builtin___build_class___obj);
diff --git a/py/builtinimport.c b/py/builtinimport.c
index cd9636ccd..c6d601144 100644
--- a/py/builtinimport.c
+++ b/py/builtinimport.c
@@ -591,6 +591,14 @@ mp_obj_t mp_builtin___import__(size_t n_args, const mp_obj_t *args) {
     }
     #endif
 
+    #if MICROPY_MODULE_BUILTIN_IMPORT_EXTRA
+    // Call port-specific import extension.
+    module_obj = mp_builtin_import_extra(n_args, args);
+    if (module_obj != MP_OBJ_NULL) {
+        return module_obj;
+    }
+    #endif
+
     // Couldn't find the module, so fail
     #if MICROPY_ERROR_REPORTING <= MICROPY_ERROR_REPORTING_TERSE
     mp_raise_msg(&mp_type_ImportError, MP_ERROR_TEXT("module not found"));
diff --git a/py/mpconfig.h b/py/mpconfig.h
index d70d39ae9..7a20b47c9 100644
--- a/py/mpconfig.h
+++ b/py/mpconfig.h
@@ -834,6 +834,12 @@ typedef double mp_float_t;
 #define MICROPY_MODULE_BUILTIN_INIT (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
 #endif
 
+// Hook to run port-specific import handler if standard builtin import fails.
+// This is only used if MICROPY_ENABLE_EXTERNAL_IMPORT is disabled.
+#ifndef MICROPY_MODULE_BUILTIN_IMPORT_EXTRA
+#define MICROPY_MODULE_BUILTIN_IMPORT_EXTRA (0)
+#endif
+
 // Whether to support module-level __getattr__ (see PEP 562)
 #ifndef MICROPY_MODULE_GETATTR
 #define MICROPY_MODULE_GETATTR (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES)

@dpgeorge
Copy link
Member

dpgeorge commented Aug 8, 2022

Another option would be to add a hook that gets called just prior to raising the ImportError. Ports can then fill in the gap as needed.

If done this way then for it to be general there would need to be a hook at the start and end of the import function. I think best to allow the port to override mp_builtin___import__(), but also for it to have access to the default one to call if it wants.

Eg have a mp_builtin___import___default() which is aliased to mp_builtin___import__() unless a board overrides the latter.

@laurensvalk
Copy link
Contributor Author

laurensvalk commented Aug 11, 2022

I think best to allow the port to override mp_builtin___import__(), but also for it to have access to the default one to call if it wants. (...) Eg have a mp_builtin___import___default() which is aliased to mp_builtin___import__() unless a board overrides the latter.

If I'm understanding this right, this is now done with the updated version. Just let me know if you intended something else.

This approach works fine for our use case as well.

Being more general, it can now be used even if external imports are enabled (even if that isn't all that useful).

but also for it to have access to the default one to call if it wants

One minor downside to this approach (compared to the hook approach mentioned above) is that if you just want to extend the default behavior, you have to catch the exception raised by the default handler before you can proceed.

But this is not much of a downside if external imports are disabled (which is the main use case anyway), because then mp_builtin___import___default is small enough that replicating it in full is not really an issue.

@laurensvalk laurensvalk changed the title py/builtinimport: Allow overriding mp_builtin___import__ if external imports disabled. py/builtinimport: Allow overriding of mp_builtin___import__. Aug 11, 2022
@laurensvalk laurensvalk marked this pull request as draft August 11, 2022 12:26
@laurensvalk laurensvalk marked this pull request as ready for review August 11, 2022 12:29
mp_obj_t mp_builtin___import__(size_t n_args, const mp_obj_t *args);
#else
#define mp_builtin___import__ mp_builtin___import___default
Copy link
Member

Choose a reason for hiding this comment

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

please use a static inline definition here, it's more robust than a #define

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done and rebased. It does increase the code size by a few bytes (+4 on minimal embedded port).

Copy link
Member

Choose a reason for hiding this comment

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

Ah! So the reason is because of this line:

MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_builtin___import___obj, 1, 5, mp_builtin___import__);

which forces both the default and static-inline wrapper function to be linked in to the firmware (instead of just the default one).

I guess we need to go back to using the #define to make it efficient...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! I was wondering about that. I reverted it and rebased.

Copy link
Member

Choose a reason for hiding this comment

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

Sorry to go back and forth on this.... We don't really have a consistent way to override an arbitrary function in the core, but there is some precedence set by py/mphal.h. Following the way it does things, we could do the following here:

#ifndef mp_builtin___import__
#define mp_builtin___import__ mp_builtin___import___default
#endif

That would be pretty clean, and doesn't require an entry in py/mpconfig.h. It's also how ports/stm32/boardctrl.h does things.

Then in your port you do:

mp_obj_t custom_import(size_t n_args, const mp_obj_t *args);
#define mp_builtin___import__ custom_import

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 one of the first approaches I tried, but I got a bit stuck on where to do this exactly.

I'm able to make this work following the same mphal approach (the ifndef in py/mphal.h and custom_import in mphalport.h), but I figured this one doesn't belong there since it doesn't concern hardware.

It seems like the custom implementation would be provided via mpconfigport.h, but I'm not sure how since the declaration relies on mp_obj_t which isn't defined at that point.

What would be the right places to put the suggested snippets above?

Copy link
Member

Choose a reason for hiding this comment

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

It should go here in py/builtin.h.

How about this:

#ifndef mp_builtin___import__
#define mp_builtin___import__ mp_builtin___import___default
#endif
mp_obj_t mp_builtin___import__(size_t n_args, const mp_obj_t *args);
mp_obj_t mp_builtin___import__default(size_t n_args, const mp_obj_t *args);

?

It's a bit tricky but should work.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice, that works! Updated and rebased.

@dpgeorge
Copy link
Member

I think the approach here is fine, it should be rare that a port wants to override this import behaviour.

@laurensvalk laurensvalk force-pushed the override-builtin-import branch 2 times, most recently from 167f980 to 10a752f Compare August 11, 2022 18:06
@dpgeorge dpgeorge added the py-core Relates to py/ directory in source label Aug 12, 2022
@laurensvalk laurensvalk force-pushed the override-builtin-import branch from 10a752f to ac16032 Compare August 12, 2022 13:43
@laurensvalk laurensvalk force-pushed the override-builtin-import branch from ac16032 to 68e7455 Compare August 19, 2022 14:57
This allows ports to override mp_builtin___import__.

This can be useful in MicroPython applications where
MICROPY_ENABLE_EXTERNAL_IMPORT has to be disabled due to its impact on
build size (2% to 2.5% of the minimal port). By overriding the otherwise
very minimal mp_builtin___import__, ports can still allow limited forms
of application-specific imports.

Signed-off-by: Laurens Valk <laurens@pybricks.com>
@laurensvalk laurensvalk force-pushed the override-builtin-import branch from 68e7455 to aa76e8a Compare August 19, 2022 15:01
@dpgeorge
Copy link
Member

Thanks for updating. Merged in d8ad878

@dpgeorge dpgeorge closed this Aug 23, 2022
@dlech dlech deleted the override-builtin-import branch August 23, 2022 20:29
tannewt added a commit to tannewt/circuitpython that referenced this pull request Mar 11, 2024
It is likely due to the multiple heap setup on ESP32. An allocation
fails into one heap because the size is too large but caused an
assertion instead of failing over to the next heap.

Fixes micropython#9026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
py-core Relates to py/ directory in source
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants