Files
mtgodot-poc/extension/tests/port_python_launcher_test.cpp
T
shenleiandClaude Opus 5 43bb561551 port(2P step 2): ScriptLib/PythonLauncher on the embedded interpreter
Link mtpython into port_logic and copy 40250's ScriptLib launcher across:
PythonLauncher, PythonUtils and PythonMarshal, plus the 12 forwarding headers
that make its `#include <Python-2.7/*>` lines compile unchanged. symtable.h
forward-declares mod_ty rather than including Python-ast.h, whose one-word
macros (Delete, Module, Set, Print) collide with EterLib/Pool.h — the same
clash 40250 sidestepped by commenting out PySymtable_Build.

Two `// PORT:` edits: `unsigned char(val)` is an MSVC extension (PythonUtils.cpp,
twice), and PyObject_AsCharBuffer wants a Py_ssize_t* (TraceFunc).

port_python_launcher_test drives the launcher in the order UserInterface.cpp
does (:241-434): Create(), __DEBUG__/__COMMAND_LINE__, TRUE/FALSE, a CRLF script
through RunMemoryTextFile, a .pyc through RunCompiledFile, then RunFile reading
system.py out of the real pack. It stops at `ImportError: No module named app`,
i.e. the first C++ module RunMainScript registers — that is the 2V0 slice, and
the stdlib behind it is step 3. The host flags (Py_NoSiteFlag and friends) sit
in the test for now; step 3 moves them into the platform layer.

port-map statuses are set only for what the test actually executes: 10 of 12
launcher functions and 4 of 11 marshal ones (the small-.pyc path delegates to
CPython's own reader, so the ported r_object stays unreached). PythonUtils
compiles but nothing calls it until the binding modules arrive, so it stays TODO.

Windows has no mtpython yet, so port_logic drops ScriptLib and the shims there.

Tests: macOS ctest 27/27 incl. port.python_launcher; port_logic (with ScriptLib)
also compiles for android arm64 (NDK API 24) and ios arm64.

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

189 lines
8.0 KiB
C++

// port/ScriptLib CPythonLauncher on the embedded interpreter (批次 2P step 2), driven exactly as
// 40250 UserInterface.cpp does it: Main() creates the launcher, calls Create(), and RunMainScript()
// then sets __DEBUG__ and __COMMAND_LINE__ with RunLine before RunFile("system.py") reads the script
// out of the registered packs (UserInterface.cpp:241-353, 419-434).
//
// The script layer's own modules (initapp, initpack, initwndMgr, ...) arrive with the 2V0 slice and the
// stdlib with step 3, so system.py is expected to stop at its first import ("No module named app") —
// what this test proves is the launcher: the interpreter starts, RunLine/RunMemoryTextFile/
// RunCompiledFile run code, and RunFile reaches the real script through CEterPackManager.
//
// port_python_launcher_test <40250 Client dir>
//
// Exit 77 (ctest SKIP) when the client is missing, unless MT_ASSETS_STRICT=1, which fails instead.
#include "ScriptLib/StdAfx.h"
#include "ScriptLib/PythonLauncher.h"
#include "EterPack/EterPackManager.h"
#include "../src/platform/UserInterface/UserInterface.h"
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <unistd.h>
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)
// The dictionary the launcher runs everything in, and the globals it leaves behind there.
static PyObject* main_dict()
{
return PyModule_GetDict(PyImport_AddModule((char*) "__main__"));
}
static long main_long(const char* name)
{
PyObject* main_module = PyImport_AddModule((char*) "__main__");
PyObject* value = PyDict_GetItemString(PyModule_GetDict(main_module), name);
return value && PyInt_Check(value) ? PyInt_AsLong(value) : -1;
}
static std::string main_string(const char* name)
{
PyObject* main_module = PyImport_AddModule((char*) "__main__");
PyObject* value = PyDict_GetItemString(PyModule_GetDict(main_module), name);
return value && PyString_Check(value) ? PyString_AS_STRING(value) : "";
}
int main(int argc, char** argv)
{
const char* strict = std::getenv("MT_ASSETS_STRICT");
const bool is_strict = strict && std::string(strict) == "1";
const std::string client = argc > 1 ? argv[1] : "";
if (client.empty() || chdir(client.c_str()) != 0 || access("pack/Index", 0) != 0)
{
std::fprintf(stderr, "port_python_launcher_test: no 40250 Client/pack at '%s'\n", client.c_str());
return is_strict ? 1 : 77;
}
PackSingletons();
CHECK(PackInitialize("pack"));
// Host policy, set before the launcher's ctor runs Py_Initialize: 40250 shipped a Python with a real
// prefix on disk, the embedded one has none, so `import site` would fail at startup. The flags are the
// ones tools/py_embed_android/main.c uses; step 3 moves them, and the stdlib on sys.path, into the
// platform layer that every host shares.
Py_NoSiteFlag = 1;
Py_IgnoreEnvironmentFlag = 1;
Py_FrozenFlag = 1;
Py_DontWriteBytecodeFlag = 1;
Py_SetProgramName((char*) "eter.python"); // CPythonLauncher::Create's default program name
{
CPythonLauncher launcher; // ctor runs Py_Initialize, as 40250 Main() does on the stack
CHECK(Py_IsInitialized());
CHECK(launcher.Create());
// RegisterDebugFlag / RegisterCommandLine, verbatim from RunMainScript.
CHECK(launcher.RunLine("__DEBUG__ = 0"));
CHECK(launcher.RunLine("__COMMAND_LINE__ = \"\""));
CHECK(main_long("__DEBUG__") == 0);
CHECK(main_string("__COMMAND_LINE__") == "");
// Create() adds TRUE/FALSE to __builtin__, which the 40250 scripts use unqualified.
CHECK(launcher.RunLine("__ok__ = (TRUE, FALSE)"));
CHECK(launcher.RunLine("assert TRUE == 1 and FALSE == 0"));
// A failing line reports through Traceback(), which fetches (and so clears) the exception.
CHECK(!launcher.RunLine("raise RuntimeError('port test')"));
CHECK(PyErr_Occurred() == NULL);
// GetError() reads the pending exception's value with neither normalization nor a NULL check,
// so it is only defined while an error really is pending and was set with a string value — after
// RunLine it never is, because Traceback() got there first. Kept verbatim: it is what 40250 does.
PyErr_SetString(PyExc_RuntimeError, "port test error");
CHECK(std::string(launcher.GetError()) == "port test error");
PyErr_Clear();
// RunMemoryTextFile wraps the text in exec(compile(...)) after dropping CR, so a CRLF script
// from the pack compiles and the file name it is given reaches the code object.
const char crlf_script[] = "__mem__ = 1\r\ndef f():\r\n\treturn f.func_code.co_filename\r\n__mem__ = f()\r\n";
CHECK(launcher.RunMemoryTextFile("crlf.py", sizeof(crlf_script) - 1, crlf_script));
CHECK(main_string("__mem__") == "crlf.py");
// RunCompiledFile reads a .pyc through the ported PythonMarshal readers: magic, timestamp,
// then the code object.
const std::string pyc = std::string(std::getenv("TMPDIR") ? std::getenv("TMPDIR") : "/tmp") +
"/port_python_launcher_test.pyc";
PyObject* code = Py_CompileString("__pyc__ = 7\n", "compiled.py", Py_file_input);
CHECK(code != NULL);
if (code)
{
FILE* fp = fopen(pyc.c_str(), "wb");
CHECK(fp != NULL);
if (fp)
{
PyMarshal_WriteLongToFile(PyImport_GetMagicNumber(), fp, Py_MARSHAL_VERSION);
PyMarshal_WriteLongToFile(0, fp, Py_MARSHAL_VERSION); // mtime, ignored on read
PyMarshal_WriteObjectToFile(code, fp, Py_MARSHAL_VERSION);
fclose(fp);
CHECK(launcher.RunCompiledFile(pyc.c_str()));
CHECK(main_long("__pyc__") == 7);
}
Py_DECREF(code);
std::remove(pyc.c_str());
}
CHECK(!launcher.RunCompiledFile("no/such.pyc"));
// RunFile pulls the script out of the packs. system.py is the 40250 entry point; without the
// stdlib (step 3) and the C++ modules (2V0) it stops at its first import, which is still proof
// that the pack read path and the compile reached Python.
{
CMappedFile file;
const VOID* data = NULL;
CHECK(CEterPackManager::Instance().Get(file, "system.py", &data));
CHECK(file.Size() > 0);
// Where it stops, spelled out: RunFile reports through Traceback(), which fetches the
// exception and so leaves nothing behind, and LogBoxf is a platform stub. Running the same
// source here once names it: today "No module named app", i.e. system.py compiles and runs
// until the first of the C++ modules RunMainScript registers (initapp, 2V0 slice) — the
// stdlib imports behind it come with step 3.
std::string source((const char*) data, file.Size());
source.erase(std::remove(source.begin(), source.end(), '\r'), source.end());
PyObject* result = PyRun_String(source.c_str(), Py_file_input, main_dict(), main_dict());
CHECK(result == NULL);
if (result)
Py_DECREF(result);
else
{
PyObject *exc = NULL, *value = NULL, *tb = NULL;
PyErr_Fetch(&exc, &value, &tb);
PyErr_NormalizeException(&exc, &value, &tb);
PyObject* text = value ? PyObject_Str(value) : NULL;
std::printf("system.py stops at %s: %s\n",
exc ? ((PyTypeObject*) exc)->tp_name : "?",
text ? PyString_AsString(text) : "");
CHECK(exc == PyExc_ImportError);
Py_XDECREF(text);
Py_XDECREF(exc);
Py_XDECREF(value);
Py_XDECREF(tb);
}
}
// Whatever it stops at, RunLine's Traceback() has already reported and cleared it, so there is
// nothing left for GetError() to read here either.
const bool ran = launcher.RunFile("system.py");
CHECK(!ran);
CHECK(PyErr_Occurred() == NULL);
CHECK(!launcher.RunFile("no/such/script.py")); // not in any pack: no file, no exception
launcher.Clear(); // Main() calls Clear() explicitly before the launcher leaves scope
}
CHECK(!Py_IsInitialized());
if (g_failures)
std::fprintf(stderr, "%d failure(s)\n", g_failures);
else
std::printf("port_python_launcher_test: PASS\n");
return g_failures ? 1 : 0;
}