Skip to content

Commit 1e4e59b

Browse files
itamarokumaraditya303encukouvstinner
authored
pythongh-116146: Add C-API to create module from spec and initfunc (pythonGH-139196)
Co-authored-by: Kumar Aditya <[email protected]> Co-authored-by: Petr Viktorin <[email protected]> Co-authored-by: Victor Stinner <[email protected]>
1 parent a4dd662 commit 1e4e59b

File tree

7 files changed

+223
-21
lines changed

7 files changed

+223
-21
lines changed

Doc/c-api/import.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,3 +333,24 @@ Importing Modules
333333
strings instead of Python :class:`str` objects.
334334
335335
.. versionadded:: 3.14
336+
337+
.. c:function:: PyObject* PyImport_CreateModuleFromInitfunc(PyObject *spec, PyObject* (*initfunc)(void))
338+
339+
This function is a building block that enables embedders to implement
340+
the :py:meth:`~importlib.abc.Loader.create_module` step of custom
341+
static extension importers (e.g. of statically-linked extensions).
342+
343+
*spec* must be a :class:`~importlib.machinery.ModuleSpec` object.
344+
345+
*initfunc* must be an :ref:`initialization function <extension-export-hook>`,
346+
the same as for :c:func:`PyImport_AppendInittab`.
347+
348+
On success, create and return a module object.
349+
This module will not be initialized; call :c:func:`!PyModule_Exec`
350+
to initialize it.
351+
(Custom importers should do this in their
352+
:py:meth:`~importlib.abc.Loader.exec_module` method.)
353+
354+
On error, return NULL with an exception set.
355+
356+
.. versionadded:: next

Doc/whatsnew/3.15.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,6 +1080,10 @@ New features
10801080
thread state.
10811081
(Contributed by Victor Stinner in :gh:`139653`.)
10821082

1083+
* Add a new :c:func:`PyImport_CreateModuleFromInitfunc` C-API for creating
1084+
a module from a *spec* and *initfunc*.
1085+
(Contributed by Itamar Oren in :gh:`116146`.)
1086+
10831087

10841088
Changed C APIs
10851089
--------------

Include/cpython/import.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ struct _inittab {
1010
PyAPI_DATA(struct _inittab *) PyImport_Inittab;
1111
PyAPI_FUNC(int) PyImport_ExtendInittab(struct _inittab *newtab);
1212

13+
// Custom importers may use this API to initialize statically linked
14+
// extension modules directly from a spec and init function,
15+
// without needing to go through inittab
16+
PyAPI_FUNC(PyObject *) PyImport_CreateModuleFromInitfunc(
17+
PyObject *spec,
18+
PyObject *(*initfunc)(void));
19+
1320
struct _frozen {
1421
const char *name; /* ASCII encoded string */
1522
const unsigned char *code;

Lib/test/test_embed.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,31 @@ def test_repeated_init_and_inittab(self):
239239
lines = "\n".join(lines) + "\n"
240240
self.assertEqual(out, lines)
241241

242+
def test_create_module_from_initfunc(self):
243+
out, err = self.run_embedded_interpreter("test_create_module_from_initfunc")
244+
if support.Py_GIL_DISABLED:
245+
# the test imports a singlephase init extension, so it emits a warning
246+
# under the free-threaded build
247+
expected_runtime_warning = (
248+
"RuntimeWarning: The global interpreter lock (GIL)"
249+
" has been enabled to load module 'embedded_ext'"
250+
)
251+
filtered_err_lines = [
252+
line
253+
for line in err.strip().splitlines()
254+
if expected_runtime_warning not in line
255+
]
256+
self.assertEqual(filtered_err_lines, [])
257+
else:
258+
self.assertEqual(err, "")
259+
self.assertEqual(out,
260+
"<module 'my_test_extension' (static-extension)>\n"
261+
"my_test_extension.executed='yes'\n"
262+
"my_test_extension.exec_slot_ran='yes'\n"
263+
"<module 'embedded_ext' (static-extension)>\n"
264+
"embedded_ext.executed='yes'\n"
265+
)
266+
242267
def test_forced_io_encoding(self):
243268
# Checks forced configuration of embedded interpreter IO streams
244269
env = dict(os.environ, PYTHONIOENCODING="utf-8:surrogateescape")
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add a new :c:func:`PyImport_CreateModuleFromInitfunc` C-API for creating a
2+
module from a *spec* and *initfunc*. Patch by Itamar Oren.

Programs/_testembed.c

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ static PyModuleDef embedded_ext = {
166166
static PyObject*
167167
PyInit_embedded_ext(void)
168168
{
169+
// keep this as a single-phase initialization module;
170+
// see test_create_module_from_initfunc
169171
return PyModule_Create(&embedded_ext);
170172
}
171173

@@ -1894,8 +1896,16 @@ static int test_initconfig_exit(void)
18941896
}
18951897

18961898

1899+
int
1900+
extension_module_exec(PyObject *mod)
1901+
{
1902+
return PyModule_AddStringConstant(mod, "exec_slot_ran", "yes");
1903+
}
1904+
1905+
18971906
static PyModuleDef_Slot extension_slots[] = {
18981907
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
1908+
{Py_mod_exec, extension_module_exec},
18991909
{0, NULL}
19001910
};
19011911

@@ -2213,6 +2223,106 @@ static int test_repeated_init_and_inittab(void)
22132223
return 0;
22142224
}
22152225

2226+
static PyObject*
2227+
create_module(PyObject* self, PyObject* spec)
2228+
{
2229+
PyObject *name = PyObject_GetAttrString(spec, "name");
2230+
if (!name) {
2231+
return NULL;
2232+
}
2233+
if (PyUnicode_EqualToUTF8(name, "my_test_extension")) {
2234+
Py_DECREF(name);
2235+
return PyImport_CreateModuleFromInitfunc(spec, init_my_test_extension);
2236+
}
2237+
if (PyUnicode_EqualToUTF8(name, "embedded_ext")) {
2238+
Py_DECREF(name);
2239+
return PyImport_CreateModuleFromInitfunc(spec, PyInit_embedded_ext);
2240+
}
2241+
PyErr_Format(PyExc_LookupError, "static module %R not found", name);
2242+
Py_DECREF(name);
2243+
return NULL;
2244+
}
2245+
2246+
static PyObject*
2247+
exec_module(PyObject* self, PyObject* mod)
2248+
{
2249+
if (PyModule_Exec(mod) < 0) {
2250+
return NULL;
2251+
}
2252+
Py_RETURN_NONE;
2253+
}
2254+
2255+
static PyMethodDef create_static_module_methods[] = {
2256+
{"create_module", create_module, METH_O, NULL},
2257+
{"exec_module", exec_module, METH_O, NULL},
2258+
{}
2259+
};
2260+
2261+
static struct PyModuleDef create_static_module_def = {
2262+
PyModuleDef_HEAD_INIT,
2263+
.m_name = "create_static_module",
2264+
.m_size = 0,
2265+
.m_methods = create_static_module_methods,
2266+
.m_slots = extension_slots,
2267+
};
2268+
2269+
PyMODINIT_FUNC PyInit_create_static_module(void) {
2270+
return PyModuleDef_Init(&create_static_module_def);
2271+
}
2272+
2273+
static int
2274+
test_create_module_from_initfunc(void)
2275+
{
2276+
wchar_t* argv[] = {
2277+
PROGRAM_NAME,
2278+
L"-c",
2279+
// Multi-phase initialization
2280+
L"import my_test_extension;"
2281+
L"print(my_test_extension);"
2282+
L"print(f'{my_test_extension.executed=}');"
2283+
L"print(f'{my_test_extension.exec_slot_ran=}');"
2284+
// Single-phase initialization
2285+
L"import embedded_ext;"
2286+
L"print(embedded_ext);"
2287+
L"print(f'{embedded_ext.executed=}');"
2288+
};
2289+
PyConfig config;
2290+
if (PyImport_AppendInittab("create_static_module",
2291+
&PyInit_create_static_module) != 0) {
2292+
fprintf(stderr, "PyImport_AppendInittab() failed\n");
2293+
return 1;
2294+
}
2295+
PyConfig_InitPythonConfig(&config);
2296+
config.isolated = 1;
2297+
config_set_argv(&config, Py_ARRAY_LENGTH(argv), argv);
2298+
init_from_config_clear(&config);
2299+
int result = PyRun_SimpleString(
2300+
"import sys\n"
2301+
"from importlib.util import spec_from_loader\n"
2302+
"import create_static_module\n"
2303+
"class StaticExtensionImporter:\n"
2304+
" _ORIGIN = \"static-extension\"\n"
2305+
" @classmethod\n"
2306+
" def find_spec(cls, fullname, path, target=None):\n"
2307+
" if fullname in {'my_test_extension', 'embedded_ext'}:\n"
2308+
" return spec_from_loader(fullname, cls, origin=cls._ORIGIN)\n"
2309+
" return None\n"
2310+
" @staticmethod\n"
2311+
" def create_module(spec):\n"
2312+
" return create_static_module.create_module(spec)\n"
2313+
" @staticmethod\n"
2314+
" def exec_module(module):\n"
2315+
" create_static_module.exec_module(module)\n"
2316+
" module.executed = 'yes'\n"
2317+
"sys.meta_path.append(StaticExtensionImporter)\n"
2318+
);
2319+
if (result < 0) {
2320+
fprintf(stderr, "PyRun_SimpleString() failed\n");
2321+
return 1;
2322+
}
2323+
return Py_RunMain();
2324+
}
2325+
22162326
static void wrap_allocator(PyMemAllocatorEx *allocator);
22172327
static void unwrap_allocator(PyMemAllocatorEx *allocator);
22182328

@@ -2396,6 +2506,7 @@ static struct TestCase TestCases[] = {
23962506
#endif
23972507
{"test_get_incomplete_frame", test_get_incomplete_frame},
23982508
{"test_gilstate_after_finalization", test_gilstate_after_finalization},
2509+
{"test_create_module_from_initfunc", test_create_module_from_initfunc},
23992510
{NULL, NULL}
24002511
};
24012512

Python/import.c

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2364,8 +2364,23 @@ is_builtin(PyObject *name)
23642364
return 0;
23652365
}
23662366

2367+
static PyModInitFunction
2368+
lookup_inittab_initfunc(const struct _Py_ext_module_loader_info* info)
2369+
{
2370+
for (struct _inittab *p = INITTAB; p->name != NULL; p++) {
2371+
if (_PyUnicode_EqualToASCIIString(info->name, p->name)) {
2372+
return (PyModInitFunction)p->initfunc;
2373+
}
2374+
}
2375+
// not found
2376+
return NULL;
2377+
}
2378+
23672379
static PyObject*
2368-
create_builtin(PyThreadState *tstate, PyObject *name, PyObject *spec)
2380+
create_builtin(
2381+
PyThreadState *tstate, PyObject *name,
2382+
PyObject *spec,
2383+
PyModInitFunction initfunc)
23692384
{
23702385
struct _Py_ext_module_loader_info info;
23712386
if (_Py_ext_module_loader_info_init_for_builtin(&info, name) < 0) {
@@ -2396,25 +2411,15 @@ create_builtin(PyThreadState *tstate, PyObject *name, PyObject *spec)
23962411
_extensions_cache_delete(info.path, info.name);
23972412
}
23982413

2399-
struct _inittab *found = NULL;
2400-
for (struct _inittab *p = INITTAB; p->name != NULL; p++) {
2401-
if (_PyUnicode_EqualToASCIIString(info.name, p->name)) {
2402-
found = p;
2403-
break;
2404-
}
2405-
}
2406-
if (found == NULL) {
2407-
// not found
2408-
mod = Py_NewRef(Py_None);
2409-
goto finally;
2410-
}
2411-
2412-
PyModInitFunction p0 = (PyModInitFunction)found->initfunc;
2414+
PyModInitFunction p0 = initfunc;
24132415
if (p0 == NULL) {
2414-
/* Cannot re-init internal module ("sys" or "builtins") */
2415-
assert(is_core_module(tstate->interp, info.name, info.path));
2416-
mod = import_add_module(tstate, info.name);
2417-
goto finally;
2416+
p0 = lookup_inittab_initfunc(&info);
2417+
if (p0 == NULL) {
2418+
/* Cannot re-init internal module ("sys" or "builtins") */
2419+
assert(is_core_module(tstate->interp, info.name, info.path));
2420+
mod = import_add_module(tstate, info.name);
2421+
goto finally;
2422+
}
24182423
}
24192424

24202425
#ifdef Py_GIL_DISABLED
@@ -2440,6 +2445,33 @@ create_builtin(PyThreadState *tstate, PyObject *name, PyObject *spec)
24402445
return mod;
24412446
}
24422447

2448+
PyObject*
2449+
PyImport_CreateModuleFromInitfunc(
2450+
PyObject *spec, PyObject *(*initfunc)(void))
2451+
{
2452+
if (initfunc == NULL) {
2453+
PyErr_BadInternalCall();
2454+
return NULL;
2455+
}
2456+
2457+
PyThreadState *tstate = _PyThreadState_GET();
2458+
2459+
PyObject *name = PyObject_GetAttr(spec, &_Py_ID(name));
2460+
if (name == NULL) {
2461+
return NULL;
2462+
}
2463+
2464+
if (!PyUnicode_Check(name)) {
2465+
PyErr_Format(PyExc_TypeError,
2466+
"spec name must be string, not %T", name);
2467+
Py_DECREF(name);
2468+
return NULL;
2469+
}
2470+
2471+
PyObject *mod = create_builtin(tstate, name, spec, initfunc);
2472+
Py_DECREF(name);
2473+
return mod;
2474+
}
24432475

24442476
/*****************************/
24452477
/* the builtin modules table */
@@ -3209,7 +3241,7 @@ bootstrap_imp(PyThreadState *tstate)
32093241
}
32103242

32113243
// Create the _imp module from its definition.
3212-
PyObject *mod = create_builtin(tstate, name, spec);
3244+
PyObject *mod = create_builtin(tstate, name, spec, NULL);
32133245
Py_CLEAR(name);
32143246
Py_DECREF(spec);
32153247
if (mod == NULL) {
@@ -4369,7 +4401,7 @@ _imp_create_builtin(PyObject *module, PyObject *spec)
43694401
return NULL;
43704402
}
43714403

4372-
PyObject *mod = create_builtin(tstate, name, spec);
4404+
PyObject *mod = create_builtin(tstate, name, spec, NULL);
43734405
Py_DECREF(name);
43744406
return mod;
43754407
}

0 commit comments

Comments
 (0)