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>
108 lines
4.5 KiB
C++
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;
|
|
}
|