pynect 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +3 -3
  2. package/binding.gyp +2 -1
  3. package/dist/config/project.d.ts +1 -0
  4. package/dist/config/project.d.ts.map +1 -1
  5. package/dist/config/project.js +3 -2
  6. package/dist/config/project.js.map +1 -1
  7. package/dist/core/py-runtime.d.ts +3 -0
  8. package/dist/core/py-runtime.d.ts.map +1 -1
  9. package/dist/core/py-runtime.js +20 -3
  10. package/dist/core/py-runtime.js.map +1 -1
  11. package/dist/env/project-environment.d.ts.map +1 -1
  12. package/dist/env/project-environment.js +1 -0
  13. package/dist/env/project-environment.js.map +1 -1
  14. package/dist/native/native-addon-loader.js +19 -5
  15. package/dist/native/native-addon-loader.js.map +1 -1
  16. package/dist/types/addon.d.ts +4 -0
  17. package/dist/types/addon.d.ts.map +1 -1
  18. package/dist/types/addon.js.map +1 -1
  19. package/package.json +2 -2
  20. package/prebuilds/darwin-arm64/pynect-prebuild.json +5 -4
  21. package/prebuilds/darwin-arm64/pynect_addon.node +0 -0
  22. package/prebuilds/darwin-x64/pynect-prebuild.json +5 -4
  23. package/prebuilds/darwin-x64/pynect_addon.node +0 -0
  24. package/prebuilds/linux-arm64/pynect-prebuild.json +5 -4
  25. package/prebuilds/linux-arm64/pynect_addon.node +0 -0
  26. package/prebuilds/linux-x64/pynect-prebuild.json +5 -4
  27. package/prebuilds/linux-x64/pynect_addon.node +0 -0
  28. package/prebuilds/win32-arm64/pynect-prebuild.json +5 -4
  29. package/prebuilds/win32-arm64/pynect_addon.node +0 -0
  30. package/prebuilds/win32-x64/pynect-prebuild.json +5 -4
  31. package/prebuilds/win32-x64/pynect_addon.node +0 -0
  32. package/scripts/build-native.js +9 -3
  33. package/scripts/ensure-native.js +8 -2
  34. package/scripts/smoke-python-boundary.js +1 -1
  35. package/src/native/native-addon-loader.ts +26 -7
  36. package/src/native/pynect_addon.cpp +8 -0
  37. package/src/native/python_bridge.cpp +105 -40
  38. package/src/native/python_bridge.h +6 -0
  39. package/src/native/type_converter.cpp +32 -22
@@ -1,10 +1,11 @@
1
1
  {
2
- "schemaVersion": 1,
2
+ "schemaVersion": 2,
3
3
  "platform": "linux",
4
4
  "architecture": "arm64",
5
- "pythonMajorMinor": "3.12",
6
- "pythonLibraryName": "libpython3.12.so",
5
+ "pythonMinimumVersion": "3.10",
6
+ "pythonAbi": "cp310-abi3",
7
+ "pythonLibraryName": "libpython3.so",
7
8
  "napi": 8,
8
- "nativeFingerprint": "sha256:b3e6e170e36500af003354a4012bed2c2d2c65a8f105a355bda0d329e0c82075",
9
+ "nativeFingerprint": "sha256:e375cc859a1ef9a6e8e72f182886c847c2489e9ed5a6017f08d43dd01425bd52",
9
10
  "glibc": "2.39"
10
11
  }
@@ -1,10 +1,11 @@
1
1
  {
2
- "schemaVersion": 1,
2
+ "schemaVersion": 2,
3
3
  "platform": "linux",
4
4
  "architecture": "x64",
5
- "pythonMajorMinor": "3.12",
6
- "pythonLibraryName": "libpython3.12.so",
5
+ "pythonMinimumVersion": "3.10",
6
+ "pythonAbi": "cp310-abi3",
7
+ "pythonLibraryName": "libpython3.so",
7
8
  "napi": 8,
8
- "nativeFingerprint": "sha256:b3e6e170e36500af003354a4012bed2c2d2c65a8f105a355bda0d329e0c82075",
9
+ "nativeFingerprint": "sha256:e375cc859a1ef9a6e8e72f182886c847c2489e9ed5a6017f08d43dd01425bd52",
9
10
  "glibc": "2.35"
10
11
  }
@@ -1,9 +1,10 @@
1
1
  {
2
- "schemaVersion": 1,
2
+ "schemaVersion": 2,
3
3
  "platform": "win32",
4
4
  "architecture": "arm64",
5
- "pythonMajorMinor": "3.12",
6
- "pythonLibraryName": "python312.lib",
5
+ "pythonMinimumVersion": "3.10",
6
+ "pythonAbi": "cp310-abi3",
7
+ "pythonLibraryName": "python3.dll",
7
8
  "napi": 8,
8
- "nativeFingerprint": "sha256:b3e6e170e36500af003354a4012bed2c2d2c65a8f105a355bda0d329e0c82075"
9
+ "nativeFingerprint": "sha256:e375cc859a1ef9a6e8e72f182886c847c2489e9ed5a6017f08d43dd01425bd52"
9
10
  }
@@ -1,9 +1,10 @@
1
1
  {
2
- "schemaVersion": 1,
2
+ "schemaVersion": 2,
3
3
  "platform": "win32",
4
4
  "architecture": "x64",
5
- "pythonMajorMinor": "3.12",
6
- "pythonLibraryName": "python312.lib",
5
+ "pythonMinimumVersion": "3.10",
6
+ "pythonAbi": "cp310-abi3",
7
+ "pythonLibraryName": "python3.dll",
7
8
  "napi": 8,
8
- "nativeFingerprint": "sha256:b3e6e170e36500af003354a4012bed2c2d2c65a8f105a355bda0d329e0c82075"
9
+ "nativeFingerprint": "sha256:e375cc859a1ef9a6e8e72f182886c847c2489e9ed5a6017f08d43dd01425bd52"
9
10
  }
@@ -14,6 +14,12 @@ async function main() {
14
14
  pythonPath: process.env.PYNECT_PYTHON,
15
15
  });
16
16
  const embedding = python.embedding;
17
+ const stableAbiLibrary = process.platform === 'win32'
18
+ ? embedding.stableAbiLibraryPath || path.join(embedding.libraryDirectory, 'python3.lib')
19
+ : '';
20
+ const linkLibrary = stableAbiLibrary && fs.existsSync(stableAbiLibrary)
21
+ ? stableAbiLibrary
22
+ : embedding.libraryPath;
17
23
  const nodeGyp = require.resolve('node-gyp/bin/node-gyp.js');
18
24
  const mode = process.argv.includes('--debug') ? '--debug' : '--release';
19
25
  const args = [nodeGyp, 'rebuild', mode];
@@ -24,8 +30,8 @@ async function main() {
24
30
  npm_config_python: python.executable,
25
31
  PYTHON: python.executable,
26
32
  PYNECT_PYTHON_INCLUDE_DIR: embedding.includeDirectory,
27
- PYNECT_PYTHON_LIBRARY: embedding.libraryPath,
28
- PYNECT_PYTHON_LIBRARY_DIR: embedding.libraryDirectory,
33
+ PYNECT_PYTHON_LIBRARY: linkLibrary,
34
+ PYNECT_PYTHON_LIBRARY_DIR: path.dirname(linkLibrary),
29
35
  PATH: [embedding.libraryDirectory, path.dirname(python.executable), process.env.PATH || '']
30
36
  .filter(Boolean)
31
37
  .join(path.delimiter),
@@ -40,7 +46,7 @@ async function main() {
40
46
  schemaVersion: 1,
41
47
  pythonExecutable: path.resolve(python.executable),
42
48
  pythonVersion: python.version,
43
- pythonLibrary: path.resolve(embedding.libraryPath),
49
+ pythonLibrary: path.resolve(linkLibrary),
44
50
  buildType: mode === '--debug' ? 'Debug' : 'Release',
45
51
  }, null, 2)}\n`,
46
52
  );
@@ -7,6 +7,7 @@ const { spawnSync } = require('child_process');
7
7
  const ROOT = path.resolve(__dirname, '..');
8
8
  const BUILD_DIRECTORY = path.join(ROOT, 'build');
9
9
  const METADATA = path.join(BUILD_DIRECTORY, 'pynect-python.json');
10
+ let lastLoadError;
10
11
 
11
12
  function normalize(value) {
12
13
  const resolved = path.resolve(value);
@@ -33,12 +34,14 @@ function matchingAddonPath() {
33
34
  }
34
35
 
35
36
  function loads() {
37
+ lastLoadError = undefined;
36
38
  const addon = matchingAddonPath();
37
39
  if (!addon || !fs.existsSync(addon)) return false;
38
40
  try {
39
41
  require(addon);
40
42
  return true;
41
- } catch {
43
+ } catch (error) {
44
+ lastLoadError = error;
42
45
  return false;
43
46
  }
44
47
  }
@@ -55,7 +58,10 @@ function build() {
55
58
  if (result.status !== 0) {
56
59
  throw new Error(`Native addon build failed with exit code ${result.status}.`);
57
60
  }
58
- if (!loads()) throw new Error('Native addon did not load after it was built.');
61
+ if (!loads()) {
62
+ const detail = lastLoadError instanceof Error ? ` ${lastLoadError.message}` : '';
63
+ throw new Error(`Native addon did not load after it was built.${detail}`);
64
+ }
59
65
  }
60
66
 
61
67
  async function main() {
@@ -77,7 +77,7 @@ async function main() {
77
77
  await pynect.shutdown();
78
78
  }
79
79
 
80
- console.log(`Python boundary verified: ${python.executable}`);
80
+ console.log(`Python ${python.version} boundary verified: ${python.executable}`);
81
81
  }
82
82
 
83
83
  function assertSuccess(result, label) {
@@ -72,10 +72,11 @@ interface NativeBuildMetadata {
72
72
  }
73
73
 
74
74
  interface NativePrebuildMetadata {
75
- schemaVersion: 1;
75
+ schemaVersion: 2;
76
76
  platform: NodeJS.Platform;
77
77
  architecture: string;
78
- pythonMajorMinor: string;
78
+ pythonMinimumVersion: string;
79
+ pythonAbi: string;
79
80
  napi: number;
80
81
  glibc?: string;
81
82
  pythonLibraryName: string;
@@ -91,17 +92,22 @@ function loadMatchingPrebuild(options: NativeAddonBuildOptions): NativeAddon {
91
92
  } catch (error) {
92
93
  throw new Error(`Native prebuild metadata is unavailable at ${metadataPath}.`, { cause: error });
93
94
  }
94
- const pythonMajorMinor = options.pythonVersion.split('.').slice(0, 2).join('.');
95
+ const pythonVersion = majorMinor(options.pythonVersion);
96
+ const minimumVersion = typeof metadata.pythonMinimumVersion === 'string'
97
+ ? majorMinor(metadata.pythonMinimumVersion)
98
+ : [Number.POSITIVE_INFINITY, 0] as const;
95
99
  if (
96
- metadata.schemaVersion !== 1 ||
100
+ metadata.schemaVersion !== 2 ||
97
101
  metadata.platform !== process.platform ||
98
102
  metadata.architecture !== process.arch ||
99
- metadata.pythonMajorMinor !== pythonMajorMinor ||
103
+ metadata.pythonAbi !== 'cp310-abi3' ||
104
+ pythonVersion[0] !== 3 ||
105
+ compareMajorMinor(pythonVersion, minimumVersion) < 0 ||
100
106
  metadata.napi > Number(process.versions.napi ?? 0)
101
107
  ) {
102
108
  throw new Error(
103
109
  `Native prebuild does not match ${process.platform}-${process.arch}, ` +
104
- `Python ${pythonMajorMinor}, N-API ${process.versions.napi ?? 'unknown'}.`,
110
+ `Python ${options.pythonVersion}, N-API ${process.versions.napi ?? 'unknown'}.`,
105
111
  );
106
112
  }
107
113
  if (process.platform === 'linux' && metadata.glibc) {
@@ -137,7 +143,7 @@ function materializePortablePrebuild(
137
143
  'node_modules',
138
144
  '.pynect',
139
145
  'native',
140
- `${process.platform}-${process.arch}-python${metadata.pythonMajorMinor}-${identity}`,
146
+ `${process.platform}-${process.arch}-${metadata.pythonAbi}-${identity}`,
141
147
  );
142
148
  const addon = path.join(directory, 'pynect_addon.node');
143
149
  const library = path.join(directory, metadata.pythonLibraryName);
@@ -216,3 +222,16 @@ function compareVersions(left: string, right: string): number {
216
222
  }
217
223
  return 0;
218
224
  }
225
+
226
+ function majorMinor(version: string): readonly [number, number] {
227
+ const match = version.match(/^(\d+)\.(\d+)/u);
228
+ if (!match) throw new Error(`Invalid Python version: ${version}`);
229
+ return [Number(match[1]), Number(match[2])];
230
+ }
231
+
232
+ function compareMajorMinor(
233
+ left: readonly [number, number],
234
+ right: readonly [number, number],
235
+ ): number {
236
+ return left[0] - right[0] || left[1] - right[1];
237
+ }
@@ -141,6 +141,12 @@ static Napi::Value InitializePython(const Napi::CallbackInfo& info) {
141
141
  std::string pythonExecutable = opts.Has("pythonExecutable") ?
142
142
  opts.Get("pythonExecutable").As<Napi::String>().Utf8Value() : "python3";
143
143
 
144
+ std::string pythonBaseExecutable = opts.Has("pythonBaseExecutable") ?
145
+ opts.Get("pythonBaseExecutable").As<Napi::String>().Utf8Value() : pythonExecutable;
146
+
147
+ std::string pythonPrefix = opts.Has("pythonPrefix") ?
148
+ opts.Get("pythonPrefix").As<Napi::String>().Utf8Value() : "";
149
+
144
150
  std::string pythonLibrary = opts.Has("pythonLibrary") ?
145
151
  opts.Get("pythonLibrary").As<Napi::String>().Utf8Value() : "";
146
152
 
@@ -180,6 +186,8 @@ static Napi::Value InitializePython(const Napi::CallbackInfo& info) {
180
186
  PythonBridge::getInstance().initializePython(
181
187
  pythonHome,
182
188
  pythonExecutable,
189
+ pythonBaseExecutable,
190
+ pythonPrefix,
183
191
  pythonLibrary,
184
192
  pythonPathExtra,
185
193
  envVars
@@ -12,6 +12,7 @@
12
12
  #include <stdexcept>
13
13
  #include <iostream>
14
14
  #include <algorithm>
15
+ #include <fstream>
15
16
 
16
17
  #if defined(__linux__) || defined(__APPLE__)
17
18
  #include <dlfcn.h>
@@ -21,6 +22,40 @@ namespace pynect {
21
22
 
22
23
  namespace {
23
24
 
25
+ void setEnvironmentVariable(const std::string& key, const std::string& value) {
26
+ #ifdef _WIN32
27
+ _putenv_s(key.c_str(), value.c_str());
28
+ #else
29
+ setenv(key.c_str(), value.c_str(), 1);
30
+ #endif
31
+ }
32
+
33
+ PyObject* evaluateSource(
34
+ const std::string& source,
35
+ const std::string& filename,
36
+ const char* mode,
37
+ PyObject* globals
38
+ ) {
39
+ PyObject* builtins = PyImport_ImportModule("builtins");
40
+ if (!builtins) return nullptr;
41
+ PyRef builtinsRef = PyRef::steal(builtins);
42
+
43
+ PyObject* compile = PyObject_GetAttrString(builtins, "compile");
44
+ if (!compile) return nullptr;
45
+ PyRef compileRef = PyRef::steal(compile);
46
+
47
+ PyObject* code = PyObject_CallFunction(
48
+ compile,
49
+ "sss",
50
+ source.c_str(),
51
+ filename.c_str(),
52
+ mode
53
+ );
54
+ if (!code) return nullptr;
55
+ PyRef codeRef = PyRef::steal(code);
56
+ return PyEval_EvalCode(code, globals, globals);
57
+ }
58
+
24
59
  #if defined(__linux__) || defined(__APPLE__)
25
60
  void promotePythonLibrary(const std::string& libraryPath) {
26
61
  if (libraryPath.empty()) {
@@ -60,6 +95,8 @@ PythonBridge& PythonBridge::getInstance() {
60
95
  void PythonBridge::initializePython(
61
96
  const std::string& pythonHome,
62
97
  const std::string& pythonExecutable,
98
+ const std::string& pythonBaseExecutable,
99
+ const std::string& pythonPrefix,
63
100
  const std::string& pythonLibrary,
64
101
  const std::vector<std::string>& extraSysPaths,
65
102
  const std::vector<std::pair<std::string, std::string>>& envVars
@@ -74,52 +111,57 @@ void PythonBridge::initializePython(
74
111
 
75
112
  // Set environment variables before initialization
76
113
  for (const auto& [key, val] : envVars) {
77
- #ifdef _WIN32
78
- _putenv_s(key.c_str(), val.c_str());
79
- #else
80
- setenv(key.c_str(), val.c_str(), 1);
81
- #endif
114
+ setEnvironmentVariable(key, val);
82
115
  }
83
116
 
84
- // Configure the interpreter with the detected executable so CPython can
85
- // resolve its standard library consistently across platforms.
86
- PyConfig config;
87
- PyConfig_InitPythonConfig(&config);
88
- PyStatus status = PyConfig_SetBytesString(
89
- &config,
90
- &config.program_name,
91
- pythonExecutable.c_str()
92
- );
93
- if (!PyStatus_Exception(status) && !pythonHome.empty()) {
94
- status = PyConfig_SetBytesString(&config, &config.home, pythonHome.c_str());
95
- }
96
- if (!PyStatus_Exception(status)) {
97
- status = Py_InitializeFromConfig(&config);
117
+ // These process-wide setters are in Python's Stable ABI. Keep their decoded
118
+ // storage alive until finalization, as required by CPython.
119
+ _programName = Py_DecodeLocale(pythonExecutable.c_str(), nullptr);
120
+ if (!_programName) {
121
+ throw std::runtime_error("Failed to decode the Python executable path");
98
122
  }
99
- std::string configError = PyStatus_Exception(status) && status.err_msg
100
- ? status.err_msg
101
- : "";
102
- PyConfig_Clear(&config);
103
- if (PyStatus_Exception(status)) {
104
- throw std::runtime_error("Failed to configure Python: " + configError);
123
+ Py_SetProgramName(_programName);
124
+ if (!pythonHome.empty()) {
125
+ _pythonHome = Py_DecodeLocale(pythonHome.c_str(), nullptr);
126
+ if (!_pythonHome) {
127
+ throw std::runtime_error("Failed to decode the Python home path");
128
+ }
129
+ Py_SetPythonHome(_pythonHome);
105
130
  }
131
+ Py_Initialize();
106
132
 
107
133
  if (!Py_IsInitialized()) {
108
134
  throw std::runtime_error("Failed to initialize Python interpreter");
109
135
  }
110
136
 
111
- // Enable thread support
112
- PyEval_InitThreads();
113
-
114
137
  // Save main thread state and release GIL so threads can run
115
138
  _mainThreadState = PyEval_SaveThread();
116
139
 
117
- // Add extra sys.path entries (done with GIL held)
118
- if (!extraSysPaths.empty()) {
140
+ // Restore the selected environment identity and add its import paths after
141
+ // Stable ABI initialization used the base installation for the stdlib.
142
+ {
119
143
  GILAcquire gil;
120
144
  PyObject* sysModule = PyImport_ImportModule("sys");
121
145
  if (sysModule) {
122
146
  PyRef sysRef = PyRef::steal(sysModule);
147
+ PyObject* executable = PyUnicode_FromString(pythonExecutable.c_str());
148
+ if (executable) {
149
+ PyRef executableRef = PyRef::steal(executable);
150
+ PyObject_SetAttrString(sysModule, "executable", executable);
151
+ }
152
+ PyObject* baseExecutable = PyUnicode_FromString(pythonBaseExecutable.c_str());
153
+ if (baseExecutable) {
154
+ PyRef baseExecutableRef = PyRef::steal(baseExecutable);
155
+ PyObject_SetAttrString(sysModule, "_base_executable", baseExecutable);
156
+ }
157
+ if (!pythonPrefix.empty()) {
158
+ PyObject* prefix = PyUnicode_FromString(pythonPrefix.c_str());
159
+ if (prefix) {
160
+ PyRef prefixRef = PyRef::steal(prefix);
161
+ PyObject_SetAttrString(sysModule, "prefix", prefix);
162
+ PyObject_SetAttrString(sysModule, "exec_prefix", prefix);
163
+ }
164
+ }
123
165
  PyObject* sysPath = PyObject_GetAttrString(sysModule, "path");
124
166
  if (sysPath) {
125
167
  PyRef pathRef = PyRef::steal(sysPath);
@@ -131,6 +173,23 @@ void PythonBridge::initializePython(
131
173
  }
132
174
  }
133
175
  }
176
+ PyObject* siteModule = PyImport_ImportModule("site");
177
+ if (siteModule) {
178
+ PyRef siteRef = PyRef::steal(siteModule);
179
+ PyObject* addSiteDirectory = PyObject_GetAttrString(siteModule, "addsitedir");
180
+ if (addSiteDirectory) {
181
+ PyRef addSiteDirectoryRef = PyRef::steal(addSiteDirectory);
182
+ for (const auto& p : extraSysPaths) {
183
+ PyObject* added = PyObject_CallFunction(addSiteDirectory, "s", p.c_str());
184
+ if (added) Py_DECREF(added);
185
+ else PyErr_Clear();
186
+ }
187
+ } else {
188
+ PyErr_Clear();
189
+ }
190
+ } else {
191
+ PyErr_Clear();
192
+ }
134
193
  }
135
194
  }
136
195
 
@@ -203,8 +262,7 @@ TaskResult PythonBridge::_makeSuccessResult(PyObject* obj) {
203
262
  }
204
263
 
205
264
  // For callable objects
206
- if (PyCallable_Check(obj) && !PyFunction_Check(obj) &&
207
- !PyModule_Check(obj) && !PyType_Check(obj)) {
265
+ if (PyCallable_Check(obj) && !PyModule_Check(obj) && !PyType_Check(obj)) {
208
266
  // Instance of a class with __call__ - usually want as reference
209
267
  result.objectId = TypeConverter::registerObject(obj);
210
268
  result.isObject = true;
@@ -563,7 +621,11 @@ bool PythonBridge::_buildCallArgs(
563
621
  argsOut = nullptr;
564
622
  return false;
565
623
  }
566
- PyTuple_SET_ITEM(argsOut, static_cast<Py_ssize_t>(i), arg); // steals reference
624
+ if (PyTuple_SetItem(argsOut, static_cast<Py_ssize_t>(i), arg) != 0) {
625
+ Py_DECREF(argsOut);
626
+ argsOut = nullptr;
627
+ return false;
628
+ }
567
629
  }
568
630
 
569
631
  // Build kwargs dict
@@ -643,7 +705,7 @@ TaskResult PythonBridge::runCodeSync(const std::string& code) {
643
705
 
644
706
  PyObject* globalDict = PyModule_GetDict(mainModule); // borrowed
645
707
 
646
- PyObject* result = PyRun_String(code.c_str(), Py_file_input, globalDict, globalDict);
708
+ PyObject* result = evaluateSource(code, "<pynect>", "exec", globalDict);
647
709
  if (!result) {
648
710
  return _makeErrorResult();
649
711
  }
@@ -661,7 +723,7 @@ TaskResult PythonBridge::evalExpressionSync(const std::string& expression) {
661
723
 
662
724
  PyObject* globalDict = PyModule_GetDict(mainModule); // borrowed
663
725
 
664
- PyObject* result = PyRun_String(expression.c_str(), Py_eval_input, globalDict, globalDict);
726
+ PyObject* result = evaluateSource(expression, "<pynect>", "eval", globalDict);
665
727
  if (!result) {
666
728
  return _makeErrorResult();
667
729
  }
@@ -671,8 +733,8 @@ TaskResult PythonBridge::evalExpressionSync(const std::string& expression) {
671
733
  }
672
734
 
673
735
  TaskResult PythonBridge::runFileSync(const std::string& filePath) {
674
- FILE* fp = fopen(filePath.c_str(), "r");
675
- if (!fp) {
736
+ std::ifstream input(filePath, std::ios::binary);
737
+ if (!input) {
676
738
  TaskResult r;
677
739
  r.success = false;
678
740
  r.errorType = "FileNotFoundError";
@@ -680,11 +742,14 @@ TaskResult PythonBridge::runFileSync(const std::string& filePath) {
680
742
  return r;
681
743
  }
682
744
 
745
+ std::ostringstream source;
746
+ source << input.rdbuf();
747
+
683
748
  PyObject* mainModule = PyImport_AddModule("__main__");
749
+ if (!mainModule) return _makeErrorResult();
684
750
  PyObject* globalDict = PyModule_GetDict(mainModule);
685
751
 
686
- PyObject* result = PyRun_File(fp, filePath.c_str(), Py_file_input, globalDict, globalDict);
687
- fclose(fp);
752
+ PyObject* result = evaluateSource(source.str(), filePath, "exec", globalDict);
688
753
 
689
754
  if (!result) {
690
755
  return _makeErrorResult();
@@ -763,7 +828,7 @@ TaskResult PythonBridge::getObjectKeysSync(uint32_t objectId) {
763
828
  PyObject* name = PyList_GetItem(dirList, i);
764
829
  if (!name || !PyUnicode_Check(name)) continue;
765
830
 
766
- const char* nameStr = PyUnicode_AsUTF8(name);
831
+ const char* nameStr = PyUnicode_AsUTF8AndSize(name, nullptr);
767
832
  if (!nameStr) continue;
768
833
 
769
834
  // Filter out private names
@@ -67,6 +67,8 @@ public:
67
67
  void initializePython(
68
68
  const std::string& pythonHome,
69
69
  const std::string& pythonExecutable,
70
+ const std::string& pythonBaseExecutable,
71
+ const std::string& pythonPrefix,
70
72
  const std::string& pythonLibrary,
71
73
  const std::vector<std::string>& extraSysPaths,
72
74
  const std::vector<std::pair<std::string, std::string>>& envVars
@@ -253,6 +255,10 @@ private:
253
255
 
254
256
  bool _initialized = false;
255
257
  PyThreadState* _mainThreadState = nullptr;
258
+ // CPython requires these buffers to remain unchanged for the process
259
+ // lifetime, so the singleton intentionally retains them after finalization.
260
+ wchar_t* _programName = nullptr;
261
+ wchar_t* _pythonHome = nullptr;
256
262
 
257
263
  // Iterator registry: iteratorId -> Python iterator object
258
264
  std::map<uint32_t, PyObject*> _iterators;