Skip to content

RFC: 'true' dynamic loading for unix port #6767

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 1 commit 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 examples/unix/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
VARIANT ?= standard
BUILD ?= build-$(VARIANT)
include ../../py/mkenv.mk
PORT_DIR ?= $(TOP)/ports/unix
BUILD_DIR = $(PORT_DIR)/$(BUILD)

CFLAGS = \
-Wall -Werror -I$(TOP) -I$(TOP)/py -I$(PORT_DIR) -I$(PORT_DIR)/variants/$(VARIANT) -I$(BUILD_DIR)

ifeq ($(MICROPY_FORCE_32BIT),1)
CC += -m32
endif

all:
$(CC) -fPIC $(CFLAGS) -c dynmod.c -o $(BUILD_DIR)/dynmod.o
$(CC) -shared -o $(BUILD_DIR)/dynmod.so $(BUILD_DIR)/dynmod.o

clean:
$(RM) $(BUILD_DIR)/dynmod.*
61 changes: 61 additions & 0 deletions examples/unix/dynmod.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#include <py/obj.h>
#include <py/runtime.h>

//Convert const char* -> qstr -> mp_objt_t.
mp_obj_t new_qstr_obj(const char *str) {
return MP_OBJ_NEW_QSTR(qstr_from_str(str));
}

//Example function which adds 1 to the integer passed.
mp_obj_t add_one(mp_obj_t arg) {
return mp_obj_new_int(mp_obj_get_int(arg) + 1);
}
MP_DEFINE_CONST_FUN_OBJ_1(add_one_obj, add_one);

//Example class which wraps an integer.
typedef struct _mp_obj_number_t {
mp_obj_base_t base;
int number;
} mp_obj_number_t;

mp_obj_t number_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
mp_arg_check_num(n_args, n_kw, 1, 1, false);
mp_obj_number_t *o = m_new_obj(mp_obj_number_t);
o->base.type = type;
o->number = mp_obj_get_int(args[0]);
return MP_OBJ_FROM_PTR(o);
}

void number_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
(void)kind;
mp_obj_number_t *self = MP_OBJ_TO_PTR(self_in);
mp_printf(print, "number(%d)", self->number);
}

mp_obj_t number_add(mp_obj_t lhs, mp_obj_t rhs) {
mp_obj_number_t *self = MP_OBJ_TO_PTR(lhs);
mp_obj_number_t *rhs_in = MP_OBJ_TO_PTR(rhs);
if (!mp_obj_is_type(rhs_in, self->base.type)) {
mp_raise_TypeError(MP_ERROR_TEXT("right-hand side must be a <number>"));
}
self->number += rhs_in->number;
return mp_const_none;
}
MP_DEFINE_CONST_FUN_OBJ_2(number_add_obj, number_add);

//Module initialization funtion called upon 'import dynmod'.
mp_obj_module_t* init_dynmod() {
mp_obj_module_t *mod = (mp_obj_module_t*) mp_obj_new_module(qstr_from_str("dynmod"));
//Add the function.
mp_obj_dict_store(MP_OBJ_FROM_PTR(mod->globals), new_qstr_obj("add_one"), MP_OBJ_FROM_PTR(&add_one_obj));
//Create clas definition and add it.
mp_obj_type_t *number_type = m_new0(mp_obj_type_t, 1);
number_type->base.type = &mp_type_type;
number_type->name = qstr_from_str("number");
number_type->make_new = number_make_new;
number_type->print = number_print;
number_type->locals_dict = mp_obj_new_dict(0);
mp_obj_dict_store(MP_OBJ_FROM_PTR(number_type->locals_dict), new_qstr_obj("add"), MP_OBJ_FROM_PTR(&number_add_obj));
mp_obj_dict_store(MP_OBJ_FROM_PTR(mod->globals), new_qstr_obj("number"), MP_OBJ_FROM_PTR(number_type));
return mod;
}
9 changes: 7 additions & 2 deletions ports/unix/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ ifeq ($(MICROPY_PY_THREAD),1)
CFLAGS_MOD += -DMICROPY_PY_THREAD=1 -DMICROPY_PY_THREAD_GIL=0
LDFLAGS_MOD += $(LIBPTHREAD)
endif
ifeq ($(MICROPY_PY_DYNLOAD),1)
CFLAGS_MOD += -DMICROPY_MODULE_LOADDYNLIB=1
LDFLAGS_MOD += -ldl -rdynamic
SRC_MOD += loaddynlib.c
endif

# If the variant enables it, enable modbluetooth.
ifeq ($(MICROPY_PY_BLUETOOTH),1)
Expand Down Expand Up @@ -298,9 +303,9 @@ test: $(PROG) $(TOP)/tests/run-tests

test_full: $(PROG) $(TOP)/tests/run-tests
$(eval DIRNAME=ports/$(notdir $(CURDIR)))
cd $(TOP)/tests && MICROPY_MICROPYTHON=../$(DIRNAME)/$(PROG) ./run-tests
cd $(TOP)/tests && MICROPYPATH=:../$(DIRNAME)/$(BUILD) MICROPY_MICROPYTHON=../$(DIRNAME)/$(PROG) ./run-tests --keep-path
cd $(TOP)/tests && MICROPY_MICROPYTHON=../$(DIRNAME)/$(PROG) ./run-tests -d thread
cd $(TOP)/tests && MICROPY_MICROPYTHON=../$(DIRNAME)/$(PROG) ./run-tests --emit native
cd $(TOP)/tests && MICROPYPATH=:../$(DIRNAME)/$(BUILD) MICROPY_MICROPYTHON=../$(DIRNAME)/$(PROG) ./run-tests --emit native --keep-path
cd $(TOP)/tests && MICROPY_MICROPYTHON=../$(DIRNAME)/$(PROG) ./run-tests --via-mpy $(RUN_TESTS_MPY_CROSS_FLAGS) -d basics float micropython
cd $(TOP)/tests && MICROPY_MICROPYTHON=../$(DIRNAME)/$(PROG) ./run-tests --via-mpy $(RUN_TESTS_MPY_CROSS_FLAGS) --emit native -d basics float micropython
cat $(TOP)/tests/basics/0prelim.py | ./$(PROG) | grep -q 'abc'
Expand Down
37 changes: 37 additions & 0 deletions ports/unix/loaddynlib.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#include "py/mpconfig.h"
#include "py/obj.h"
#include <dlfcn.h>
#include <string.h>

#if 0 // print debugging info
#define DEBUG_PRINT (1)
#define DEBUG_printf DEBUG_printf
#else // don't print debugging info
#define DEBUG_PRINT (0)
#define DEBUG_printf(...) (void)0
#endif

typedef mp_obj_module_t * (*fun)();

#if defined(MICROPY_UNIX_COVERAGE)
mp_obj_module_t *mp_load_dynlib(const char *mod_name, vstr_t *path);
#endif

mp_obj_module_t *mp_load_dynlib(const char *mod_name, vstr_t *path) {
void *lib = dlopen(vstr_null_terminated_str(path), RTLD_LAZY | RTLD_LOCAL);
if (!lib) {
DEBUG_printf("dlopen %s failed: %s\n", vstr_null_terminated_str(path), dlerror());
return NULL;
}
const size_t nameLen = strlen(mod_name);
char *initFunc = (char *)alloca(nameLen + 6);
memcpy(initFunc, "init_", 5);
strcpy(initFunc + 5, mod_name);
fun f = dlsym(lib, initFunc);
if (!f) {
DEBUG_printf("dlsym %s failed: %s\n", vstr_null_terminated_str(path), dlerror());
dlclose(lib);
return NULL;
}
return f();
}
3 changes: 3 additions & 0 deletions ports/unix/mpconfigport.mk
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ MICROPY_PY_JNI = 0
# Avoid using system libraries, use copies bundled with MicroPython
# as submodules (currently affects only libffi).
MICROPY_STANDALONE = 0

# Allow import from .so files.
MICROPY_PY_DYNLOAD = 0
1 change: 1 addition & 0 deletions ports/unix/variants/coverage/mpconfigvariant.mk
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ MICROPY_ROM_TEXT_COMPRESSION = 1
MICROPY_VFS_FAT = 1
MICROPY_VFS_LFS1 = 1
MICROPY_VFS_LFS2 = 1
MICROPY_PY_DYNLOAD = 1

SRC_C += coverage.c
SRC_CXX += coveragecpp.cpp
41 changes: 40 additions & 1 deletion py/builtinimport.c
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@

#define PATH_SEP_CHAR '/'

#if MICROPY_MODULE_LOADDYNLIB
// match CPython's native module naming
#ifdef _WIN32
#ifdef _DEBUG
#define PYD_EXT "_d.pyd"
#else
#define PYD_EXT ".pyd"
#endif
#else
#define PYD_EXT ".so"
#endif

extern mp_obj_module_t *mp_load_dynlib(const char *mod_name,vstr_t *name);
#endif

bool mp_obj_is_package(mp_obj_t module) {
mp_obj_t dest[2];
mp_load_method_maybe(module, MP_QSTR___path__, dest);
Expand Down Expand Up @@ -80,6 +95,18 @@ STATIC mp_import_stat_t stat_file_py_or_mpy(vstr_t *path) {
}
#endif

#if MICROPY_MODULE_LOADDYNLIB
#if MICROPY_PERSISTENT_CODE_LOAD
vstr_cut_tail_bytes(path, 1);
#endif
vstr_cut_tail_bytes(path, 3);
vstr_add_str(path, PYD_EXT);
stat = mp_import_stat(vstr_null_terminated_str(path));
if (stat == MP_IMPORT_STAT_FILE) {
return MP_IMPORT_STAT_PYD;
}
#endif

return MP_IMPORT_STAT_NO_EXIST;
}

Expand Down Expand Up @@ -402,7 +429,19 @@ mp_obj_t mp_builtin___import__(size_t n_args, const mp_obj_t *args) {
mp_raise_msg_varg(&mp_type_ImportError, MP_ERROR_TEXT("no module named '%q'"), mod_name);
#endif
}
} else {
}
#if MICROPY_MODULE_LOADDYNLIB
else if (stat == MP_IMPORT_STAT_PYD) {
// found the file, so try to load it and get the module
mp_obj_module_t *module_ptr = mp_load_dynlib(mod_str + last, &path);
if (module_ptr == NULL) {
mp_raise_msg_varg(&mp_type_ImportError, MP_ERROR_TEXT("dynamic module load failed for '%q'"), mod_name);
}
mp_obj_dict_store(MP_OBJ_FROM_PTR(module_ptr->globals), MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(mod_name));
module_obj = MP_OBJ_FROM_PTR(module_ptr);
}
#endif
else {
// found the file, so get the module
module_obj = mp_module_get(mod_name);
}
Expand Down
3 changes: 3 additions & 0 deletions py/lexer.h
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ typedef enum {
MP_IMPORT_STAT_NO_EXIST,
MP_IMPORT_STAT_DIR,
MP_IMPORT_STAT_FILE,
#if MICROPY_MODULE_LOADDYNLIB
MP_IMPORT_STAT_PYD
#endif
} mp_import_stat_t;

mp_import_stat_t mp_import_stat(const char *path);
Expand Down
5 changes: 5 additions & 0 deletions py/mpconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,11 @@ typedef double mp_float_t;
#define MICROPY_MODULE_FROZEN (MICROPY_MODULE_FROZEN_STR || MICROPY_MODULE_FROZEN_MPY)
#endif

// Whether loading of importing dynamic libraries is supported
#ifndef MICROPY_MODULE_LOADDYNLIB
#define MICROPY_MODULE_LOADDYNLIB (0)
#endif

// Whether you can override builtins in the builtins module
#ifndef MICROPY_CAN_OVERRIDE_BUILTINS
#define MICROPY_CAN_OVERRIDE_BUILTINS (0)
Expand Down
9 changes: 9 additions & 0 deletions tests/unix/extra_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@

print(cppexample.cppfunc(1, 2))

# test import of module from .so file
import dynmod

print(dynmod.__name__)
print(dynmod.add_one(3))
number = dynmod.number(1)
number.add(dynmod.number(2))
print(number)

# test basic import of frozen scripts
import frzstr1

Expand Down
3 changes: 3 additions & 0 deletions tests/unix/extra_coverage.py.exp
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ None
cpp None
5
(3, 'hellocpp')
dynmod
4
number(3)
frzstr1
frzstr1.py
frzmpy1
Expand Down
6 changes: 6 additions & 0 deletions tools/ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,10 @@ function ci_native_mpy_modules_32bit_build {
ci_native_mpy_modules_build x86
}

function ci_unix_example_build {
make ${MAKEOPTS} -C examples/unix "$@"
}

function ci_unix_minimal_build {
make ${MAKEOPTS} -C ports/unix VARIANT=minimal
}
Expand Down Expand Up @@ -318,6 +322,7 @@ function ci_unix_coverage_setup {

function ci_unix_coverage_build {
ci_unix_build_helper VARIANT=coverage
ci_unix_example_build VARIANT=coverage
}

function ci_unix_coverage_run_tests {
Expand All @@ -342,6 +347,7 @@ function ci_unix_32bit_setup {

function ci_unix_coverage_32bit_build {
ci_unix_build_helper VARIANT=coverage MICROPY_FORCE_32BIT=1
ci_unix_example_build VARIANT=coverage MICROPY_FORCE_32BIT=1
}

function ci_unix_coverage_32bit_run_tests {
Expand Down