Files
mtgodot-poc/extension/tests/py_embed_test.cpp
T
shenleiandClaude Opus 5 5fad769ee8 port 2P step 1: vendor CPython 2.7.18 as the mtpython static library
The embedded 40250 script layer needs its own interpreter on five platforms.
2.7 is end-of-life, so nothing can be fetched from the target SDKs and the
source is vendored (official 2.7.18 tarball, trimmed to 25MB; Lib/ stays for
step 3's python27.zip).

cpython-2.7.18/CMakeLists.txt builds one `mtpython` static library from exactly
the 133 objects the reference libpython2.7.a contains, off by default behind
-DMTGODOT_EMBED_PYTHON=ON. The source list and the built-in module table
(config/config.c, 39 entries) are shared; pyconfig.h is a probe result and is
not, so each platform keeps its own under config/<platform>/, regenerated by
tools/py_embed/gen_pyconfig.sh and checked against the shared table.

Three vendor patches, documented in docs/THIRD-PARTY.md: configure/configure.ac
learn arm64 on macOS, and posixmodule.c undefines the process-control calls it
hard-defines past pyconfig.h when building for iOS.

macOS (ctest python.embed: every builtin imports, codecs and pickle work off the
vendored Lib/), Android arm64 and iOS arm64 build. Linux needs a Linux host and
Windows needs a MinGW-vs-MSVC decision; both are noted for step 4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 08:27:35 +09:00

108 lines
4.5 KiB
C++

// mtpython: the embedded CPython 2.7.18 starts, every statically linked module imports, and the
// interpreter runs code (docs/PORT-PLAN.md 批次 2P step 1). Started the way CPythonLauncher does
// (no site, no environment), then sys.path is replaced by the host, as tools/py_embed_android/main.c
// does on the device.
//
// py_embed_test [stdlib dir or python27.zip]
#include <Python.h>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
static int g_failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) \
{ \
std::fprintf(stderr, "%s:%d: CHECK(%s)\n", __FILE__, __LINE__, #cond); \
++g_failures; \
} \
} while (0)
int main(int argc, char** argv)
{
// CPythonLauncher::Create starts the interpreter without site, without environment variables and
// without a filesystem prefix; sys.path is handed over by the host afterwards.
Py_NoSiteFlag = 1;
Py_IgnoreEnvironmentFlag = 1;
Py_FrozenFlag = 1;
Py_DontWriteBytecodeFlag = 1;
Py_SetProgramName(argv[0]);
Py_Initialize();
CHECK(Py_IsInitialized());
// Without a stdlib on sys.path the interpreter still runs, but anything that imports a .py
// (cPickle -> copy_reg, the codec registry -> encodings) cannot work; the host always supplies one.
const std::string stdlib = argc > 1 ? argv[1] : "";
if (!stdlib.empty())
{
// Py_Initialize already ran _PyCodecRegistry_Init against the empty path and 2.7 never retries
// ("no codec search functions registered"), so the host re-registers encodings.search_function
// after handing over sys.path. CPythonLauncher will have to do the same.
const std::string boot = "import sys\nsys.path[:] = ['" + stdlib +
"']\nimport codecs, encodings\ncodecs.register(encodings.search_function)\n";
CHECK(PyRun_SimpleString(boot.c_str()) == 0);
}
// The built-in table is what config/config.c (shared by every platform) declares.
std::vector<std::string> names;
for (const _inittab* t = PyImport_Inittab; t && t->name; ++t)
names.push_back(t->name);
CHECK(names.size() == 39); // config/config.c: 30 from Setup.static + marshal/imp/_ast/gc/_warnings + __main__/__builtin__/sys/exceptions
size_t imported = 0;
for (const std::string& name : names)
{
if (name == "xxsubtype") // the reference Setup builds it, nothing imports it
continue;
if (name == "__main__") // importing it re-enters the running module
continue;
if (stdlib.empty() && name == "cPickle") // needs copy_reg from the stdlib
continue;
if (PyObject* m = PyImport_ImportModule(name.c_str()))
{
++imported;
Py_DECREF(m);
}
else
{
std::fprintf(stderr, "import %s failed\n", name.c_str());
PyErr_Print();
++g_failures;
}
}
// The modules the 40250 scripts reach for first, as the spike measured them.
for (const char* need : {"posix", "time", "math", "_struct", "operator", "itertools", "_collections",
"cStringIO", "cPickle", "_locale", "binascii", "datetime", "zipimport", "_codecs"})
CHECK(std::find(names.begin(), names.end(), need) != names.end());
CHECK(PyRun_SimpleString("import sys, time, math, _struct\n"
"assert sys.version.startswith('2.7.18'), sys.version\n"
"assert _struct.calcsize('<Ii') == 8\n"
"assert math.floor(2.7) == 2.0\n"
"assert len(''.join(chr(c) for c in range(32))) == 32\n") == 0);
if (!stdlib.empty())
{
// The codec registry and zipimport both go through the stdlib the host puts on sys.path.
CHECK(PyRun_SimpleString("assert u'ab'.encode('utf-8') == 'ab'\n"
"assert '\\xc3\\xa9'.decode('utf-8') == u'\\xe9'\n"
"import os, pickle, StringIO, struct, random\n"
"assert pickle.loads(pickle.dumps({'a': 1})) == {'a': 1}\n") == 0);
// 40250 text is CP949/CP1252; cp949 needs a C codec module we do not build yet (see docs).
CHECK(PyRun_SimpleString("assert u'\\xe9'.encode('cp1252') == '\\xe9'\n") == 0);
}
Py_Finalize();
CHECK(!Py_IsInitialized());
std::printf("py_embed: %zu builtin modules, %zu imported, %s\n", names.size(), imported, Py_GetVersion());
if (g_failures)
std::fprintf(stderr, "%d failure(s)\n", g_failures);
return g_failures ? 1 : 0;
}