pynect 0.2.6 → 0.2.9

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 (70) hide show
  1. package/README.md +50 -4
  2. package/binding.gyp +2 -1
  3. package/dist/cli/codegen.d.ts +3 -0
  4. package/dist/cli/codegen.d.ts.map +1 -1
  5. package/dist/cli/codegen.js +12 -0
  6. package/dist/cli/codegen.js.map +1 -1
  7. package/dist/config/project.d.ts +7 -0
  8. package/dist/config/project.d.ts.map +1 -1
  9. package/dist/config/project.js +23 -2
  10. package/dist/config/project.js.map +1 -1
  11. package/dist/core/py-proxy.d.ts +1 -1
  12. package/dist/core/py-proxy.d.ts.map +1 -1
  13. package/dist/core/py-proxy.js +40 -7
  14. package/dist/core/py-proxy.js.map +1 -1
  15. package/dist/core/py-runtime.d.ts +7 -0
  16. package/dist/core/py-runtime.d.ts.map +1 -1
  17. package/dist/core/py-runtime.js +50 -3
  18. package/dist/core/py-runtime.js.map +1 -1
  19. package/dist/core/pynect-callback.d.ts +25 -0
  20. package/dist/core/pynect-callback.d.ts.map +1 -0
  21. package/dist/core/pynect-callback.js +99 -0
  22. package/dist/core/pynect-callback.js.map +1 -0
  23. package/dist/env/project-environment.d.ts.map +1 -1
  24. package/dist/env/project-environment.js +1 -0
  25. package/dist/env/project-environment.js.map +1 -1
  26. package/dist/index.d.ts +7 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +8 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/native/native-addon-loader.d.ts.map +1 -1
  31. package/dist/native/native-addon-loader.js +34 -10
  32. package/dist/native/native-addon-loader.js.map +1 -1
  33. package/dist/types/addon.d.ts +10 -0
  34. package/dist/types/addon.d.ts.map +1 -1
  35. package/dist/types/addon.js.map +1 -1
  36. package/dist/types/config.d.ts +10 -0
  37. package/dist/types/config.d.ts.map +1 -1
  38. package/dist/types/config.js +6 -0
  39. package/dist/types/config.js.map +1 -1
  40. package/dist/types/index.d.ts +1 -1
  41. package/dist/types/index.d.ts.map +1 -1
  42. package/dist/types/index.js.map +1 -1
  43. package/dist/types/python.d.ts +2 -0
  44. package/dist/types/python.d.ts.map +1 -1
  45. package/package.json +2 -2
  46. package/prebuilds/darwin-arm64/pynect-prebuild.json +5 -4
  47. package/prebuilds/darwin-arm64/pynect_addon.node +0 -0
  48. package/prebuilds/darwin-x64/pynect-prebuild.json +5 -4
  49. package/prebuilds/darwin-x64/pynect_addon.node +0 -0
  50. package/prebuilds/linux-arm64/pynect-prebuild.json +5 -4
  51. package/prebuilds/linux-arm64/pynect_addon.node +0 -0
  52. package/prebuilds/linux-x64/pynect-prebuild.json +5 -4
  53. package/prebuilds/linux-x64/pynect_addon.node +0 -0
  54. package/prebuilds/win32-arm64/pynect-prebuild.json +5 -4
  55. package/prebuilds/win32-arm64/pynect_addon.node +0 -0
  56. package/prebuilds/win32-x64/pynect-prebuild.json +5 -4
  57. package/prebuilds/win32-x64/pynect_addon.node +0 -0
  58. package/python/type_inspector.py +12 -0
  59. package/scripts/build-native.js +9 -3
  60. package/scripts/ensure-native.js +8 -2
  61. package/scripts/smoke-python-boundary.js +71 -1
  62. package/src/native/callback_bridge.h +11 -0
  63. package/src/native/native-addon-loader.ts +39 -11
  64. package/src/native/pynect_addon.cpp +354 -4
  65. package/src/native/python_bridge.cpp +117 -40
  66. package/src/native/python_bridge.h +13 -0
  67. package/src/native/thread_pool.cpp +62 -55
  68. package/src/native/thread_pool.h +13 -0
  69. package/src/native/type_converter.cpp +32 -22
  70. package/src/native/type_converter.h +2 -0
@@ -6,12 +6,14 @@
6
6
  #include "python_bridge.h"
7
7
  #include "gil_guard.h"
8
8
  #include "type_converter.h"
9
+ #include "callback_bridge.h"
9
10
 
10
11
  #include <Python.h>
11
12
  #include <sstream>
12
13
  #include <stdexcept>
13
14
  #include <iostream>
14
15
  #include <algorithm>
16
+ #include <fstream>
15
17
 
16
18
  #if defined(__linux__) || defined(__APPLE__)
17
19
  #include <dlfcn.h>
@@ -21,6 +23,40 @@ namespace pynect {
21
23
 
22
24
  namespace {
23
25
 
26
+ void setEnvironmentVariable(const std::string& key, const std::string& value) {
27
+ #ifdef _WIN32
28
+ _putenv_s(key.c_str(), value.c_str());
29
+ #else
30
+ setenv(key.c_str(), value.c_str(), 1);
31
+ #endif
32
+ }
33
+
34
+ PyObject* evaluateSource(
35
+ const std::string& source,
36
+ const std::string& filename,
37
+ const char* mode,
38
+ PyObject* globals
39
+ ) {
40
+ PyObject* builtins = PyImport_ImportModule("builtins");
41
+ if (!builtins) return nullptr;
42
+ PyRef builtinsRef = PyRef::steal(builtins);
43
+
44
+ PyObject* compile = PyObject_GetAttrString(builtins, "compile");
45
+ if (!compile) return nullptr;
46
+ PyRef compileRef = PyRef::steal(compile);
47
+
48
+ PyObject* code = PyObject_CallFunction(
49
+ compile,
50
+ "sss",
51
+ source.c_str(),
52
+ filename.c_str(),
53
+ mode
54
+ );
55
+ if (!code) return nullptr;
56
+ PyRef codeRef = PyRef::steal(code);
57
+ return PyEval_EvalCode(code, globals, globals);
58
+ }
59
+
24
60
  #if defined(__linux__) || defined(__APPLE__)
25
61
  void promotePythonLibrary(const std::string& libraryPath) {
26
62
  if (libraryPath.empty()) {
@@ -60,6 +96,8 @@ PythonBridge& PythonBridge::getInstance() {
60
96
  void PythonBridge::initializePython(
61
97
  const std::string& pythonHome,
62
98
  const std::string& pythonExecutable,
99
+ const std::string& pythonBaseExecutable,
100
+ const std::string& pythonPrefix,
63
101
  const std::string& pythonLibrary,
64
102
  const std::vector<std::string>& extraSysPaths,
65
103
  const std::vector<std::pair<std::string, std::string>>& envVars
@@ -74,52 +112,57 @@ void PythonBridge::initializePython(
74
112
 
75
113
  // Set environment variables before initialization
76
114
  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
115
+ setEnvironmentVariable(key, val);
82
116
  }
83
117
 
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);
118
+ // These process-wide setters are in Python's Stable ABI. Keep their decoded
119
+ // storage alive until finalization, as required by CPython.
120
+ _programName = Py_DecodeLocale(pythonExecutable.c_str(), nullptr);
121
+ if (!_programName) {
122
+ throw std::runtime_error("Failed to decode the Python executable path");
98
123
  }
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);
124
+ Py_SetProgramName(_programName);
125
+ if (!pythonHome.empty()) {
126
+ _pythonHome = Py_DecodeLocale(pythonHome.c_str(), nullptr);
127
+ if (!_pythonHome) {
128
+ throw std::runtime_error("Failed to decode the Python home path");
129
+ }
130
+ Py_SetPythonHome(_pythonHome);
105
131
  }
132
+ Py_Initialize();
106
133
 
107
134
  if (!Py_IsInitialized()) {
108
135
  throw std::runtime_error("Failed to initialize Python interpreter");
109
136
  }
110
137
 
111
- // Enable thread support
112
- PyEval_InitThreads();
113
-
114
138
  // Save main thread state and release GIL so threads can run
115
139
  _mainThreadState = PyEval_SaveThread();
116
140
 
117
- // Add extra sys.path entries (done with GIL held)
118
- if (!extraSysPaths.empty()) {
141
+ // Restore the selected environment identity and add its import paths after
142
+ // Stable ABI initialization used the base installation for the stdlib.
143
+ {
119
144
  GILAcquire gil;
120
145
  PyObject* sysModule = PyImport_ImportModule("sys");
121
146
  if (sysModule) {
122
147
  PyRef sysRef = PyRef::steal(sysModule);
148
+ PyObject* executable = PyUnicode_FromString(pythonExecutable.c_str());
149
+ if (executable) {
150
+ PyRef executableRef = PyRef::steal(executable);
151
+ PyObject_SetAttrString(sysModule, "executable", executable);
152
+ }
153
+ PyObject* baseExecutable = PyUnicode_FromString(pythonBaseExecutable.c_str());
154
+ if (baseExecutable) {
155
+ PyRef baseExecutableRef = PyRef::steal(baseExecutable);
156
+ PyObject_SetAttrString(sysModule, "_base_executable", baseExecutable);
157
+ }
158
+ if (!pythonPrefix.empty()) {
159
+ PyObject* prefix = PyUnicode_FromString(pythonPrefix.c_str());
160
+ if (prefix) {
161
+ PyRef prefixRef = PyRef::steal(prefix);
162
+ PyObject_SetAttrString(sysModule, "prefix", prefix);
163
+ PyObject_SetAttrString(sysModule, "exec_prefix", prefix);
164
+ }
165
+ }
123
166
  PyObject* sysPath = PyObject_GetAttrString(sysModule, "path");
124
167
  if (sysPath) {
125
168
  PyRef pathRef = PyRef::steal(sysPath);
@@ -131,6 +174,23 @@ void PythonBridge::initializePython(
131
174
  }
132
175
  }
133
176
  }
177
+ PyObject* siteModule = PyImport_ImportModule("site");
178
+ if (siteModule) {
179
+ PyRef siteRef = PyRef::steal(siteModule);
180
+ PyObject* addSiteDirectory = PyObject_GetAttrString(siteModule, "addsitedir");
181
+ if (addSiteDirectory) {
182
+ PyRef addSiteDirectoryRef = PyRef::steal(addSiteDirectory);
183
+ for (const auto& p : extraSysPaths) {
184
+ PyObject* added = PyObject_CallFunction(addSiteDirectory, "s", p.c_str());
185
+ if (added) Py_DECREF(added);
186
+ else PyErr_Clear();
187
+ }
188
+ } else {
189
+ PyErr_Clear();
190
+ }
191
+ } else {
192
+ PyErr_Clear();
193
+ }
134
194
  }
135
195
  }
136
196
 
@@ -203,8 +263,7 @@ TaskResult PythonBridge::_makeSuccessResult(PyObject* obj) {
203
263
  }
204
264
 
205
265
  // For callable objects
206
- if (PyCallable_Check(obj) && !PyFunction_Check(obj) &&
207
- !PyModule_Check(obj) && !PyType_Check(obj)) {
266
+ if (PyCallable_Check(obj) && !PyModule_Check(obj) && !PyType_Check(obj)) {
208
267
  // Instance of a class with __call__ - usually want as reference
209
268
  result.objectId = TypeConverter::registerObject(obj);
210
269
  result.isObject = true;
@@ -563,7 +622,11 @@ bool PythonBridge::_buildCallArgs(
563
622
  argsOut = nullptr;
564
623
  return false;
565
624
  }
566
- PyTuple_SET_ITEM(argsOut, static_cast<Py_ssize_t>(i), arg); // steals reference
625
+ if (PyTuple_SetItem(argsOut, static_cast<Py_ssize_t>(i), arg) != 0) {
626
+ Py_DECREF(argsOut);
627
+ argsOut = nullptr;
628
+ return false;
629
+ }
567
630
  }
568
631
 
569
632
  // Build kwargs dict
@@ -626,6 +689,9 @@ PyObject* PythonBridge::_deserialize(const SerializedValue& value) {
626
689
  case SerializedFormat::NONE:
627
690
  Py_RETURN_NONE;
628
691
 
692
+ case SerializedFormat::JS_CALLBACK:
693
+ return createPythonCallback(value.metadata.callbackId);
694
+
629
695
  default:
630
696
  // Try JSON fallback
631
697
  if (!value.jsonData.empty()) {
@@ -635,6 +701,14 @@ PyObject* PythonBridge::_deserialize(const SerializedValue& value) {
635
701
  }
636
702
  }
637
703
 
704
+ TaskResult PythonBridge::serializeValue(PyObject* obj) {
705
+ return _makeSuccessResult(obj);
706
+ }
707
+
708
+ PyObject* PythonBridge::deserializeValue(const SerializedValue& value) {
709
+ return _deserialize(value);
710
+ }
711
+
638
712
  // Code Execution
639
713
 
640
714
  TaskResult PythonBridge::runCodeSync(const std::string& code) {
@@ -643,7 +717,7 @@ TaskResult PythonBridge::runCodeSync(const std::string& code) {
643
717
 
644
718
  PyObject* globalDict = PyModule_GetDict(mainModule); // borrowed
645
719
 
646
- PyObject* result = PyRun_String(code.c_str(), Py_file_input, globalDict, globalDict);
720
+ PyObject* result = evaluateSource(code, "<pynect>", "exec", globalDict);
647
721
  if (!result) {
648
722
  return _makeErrorResult();
649
723
  }
@@ -661,7 +735,7 @@ TaskResult PythonBridge::evalExpressionSync(const std::string& expression) {
661
735
 
662
736
  PyObject* globalDict = PyModule_GetDict(mainModule); // borrowed
663
737
 
664
- PyObject* result = PyRun_String(expression.c_str(), Py_eval_input, globalDict, globalDict);
738
+ PyObject* result = evaluateSource(expression, "<pynect>", "eval", globalDict);
665
739
  if (!result) {
666
740
  return _makeErrorResult();
667
741
  }
@@ -671,8 +745,8 @@ TaskResult PythonBridge::evalExpressionSync(const std::string& expression) {
671
745
  }
672
746
 
673
747
  TaskResult PythonBridge::runFileSync(const std::string& filePath) {
674
- FILE* fp = fopen(filePath.c_str(), "r");
675
- if (!fp) {
748
+ std::ifstream input(filePath, std::ios::binary);
749
+ if (!input) {
676
750
  TaskResult r;
677
751
  r.success = false;
678
752
  r.errorType = "FileNotFoundError";
@@ -680,11 +754,14 @@ TaskResult PythonBridge::runFileSync(const std::string& filePath) {
680
754
  return r;
681
755
  }
682
756
 
757
+ std::ostringstream source;
758
+ source << input.rdbuf();
759
+
683
760
  PyObject* mainModule = PyImport_AddModule("__main__");
761
+ if (!mainModule) return _makeErrorResult();
684
762
  PyObject* globalDict = PyModule_GetDict(mainModule);
685
763
 
686
- PyObject* result = PyRun_File(fp, filePath.c_str(), Py_file_input, globalDict, globalDict);
687
- fclose(fp);
764
+ PyObject* result = evaluateSource(source.str(), filePath, "exec", globalDict);
688
765
 
689
766
  if (!result) {
690
767
  return _makeErrorResult();
@@ -763,7 +840,7 @@ TaskResult PythonBridge::getObjectKeysSync(uint32_t objectId) {
763
840
  PyObject* name = PyList_GetItem(dirList, i);
764
841
  if (!name || !PyUnicode_Check(name)) continue;
765
842
 
766
- const char* nameStr = PyUnicode_AsUTF8(name);
843
+ const char* nameStr = PyUnicode_AsUTF8AndSize(name, nullptr);
767
844
  if (!nameStr) continue;
768
845
 
769
846
  // Filter out private names
@@ -27,6 +27,7 @@ struct CallOptions {
27
27
  std::vector<std::pair<std::string, SerializedValue>> kwargs;
28
28
  uint32_t timeoutMs = 0;
29
29
  uint32_t executionTimeoutMs = 0;
30
+ uint32_t callbackDepth = 0;
30
31
  std::string requestId;
31
32
  };
32
33
 
@@ -67,6 +68,8 @@ public:
67
68
  void initializePython(
68
69
  const std::string& pythonHome,
69
70
  const std::string& pythonExecutable,
71
+ const std::string& pythonBaseExecutable,
72
+ const std::string& pythonPrefix,
70
73
  const std::string& pythonLibrary,
71
74
  const std::vector<std::string>& extraSysPaths,
72
75
  const std::vector<std::pair<std::string, std::string>>& envVars
@@ -212,6 +215,12 @@ public:
212
215
  void collectGarbage();
213
216
  size_t getTrackedObjectCount() const;
214
217
 
218
+ /** Serialize a callback argument using the normal Python -> JavaScript rules. */
219
+ TaskResult serializeValue(PyObject* obj);
220
+
221
+ /** Deserialize a JavaScript callback result using the normal argument rules. */
222
+ PyObject* deserializeValue(const SerializedValue& value);
223
+
215
224
  private:
216
225
  PythonBridge() = default;
217
226
  ~PythonBridge() = default;
@@ -253,6 +262,10 @@ private:
253
262
 
254
263
  bool _initialized = false;
255
264
  PyThreadState* _mainThreadState = nullptr;
265
+ // CPython requires these buffers to remain unchanged for the process
266
+ // lifetime, so the singleton intentionally retains them after finalization.
267
+ wchar_t* _programName = nullptr;
268
+ wchar_t* _pythonHome = nullptr;
256
269
 
257
270
  // Iterator registry: iteratorId -> Python iterator object
258
271
  std::map<uint32_t, PyObject*> _iterators;
@@ -178,72 +178,79 @@ void PythonThreadPool::_workerThread(uint32_t threadId) {
178
178
  _stats.currentQueueSize.fetch_sub(1, std::memory_order_relaxed);
179
179
  }
180
180
 
181
- // Check timeout before executing
182
- if (_isTimedOut(task)) {
183
- TaskResult timeoutResult;
184
- timeoutResult.success = false;
185
- timeoutResult.errorType = "TimeoutError";
186
- timeoutResult.errorMessage = "Python task timed out after " +
187
- std::to_string(task.timeoutMs) + "ms in queue";
188
- timeoutResult.errorTraceback = "";
189
- _stats.totalTasksTimedOut.fetch_add(1, std::memory_order_relaxed);
190
- _postResult(task.id, std::move(timeoutResult), std::move(task.callback));
191
- continue;
192
- }
193
-
194
- // Execute the task with GIL held
195
- _stats.activeThreads.fetch_add(1, std::memory_order_relaxed);
196
-
197
- auto startTime = std::chrono::steady_clock::now();
198
- TaskResult result;
199
-
200
- try {
201
- // Acquire GIL - critical: must be released when we're done
181
+ {
202
182
  GILAcquire gil;
203
-
204
- // Check timeout again (task might have waited in queue)
205
- if (task.timeoutMs > 0 && _isTimedOut(task)) {
206
- result.success = false;
207
- result.errorType = "TimeoutError";
208
- result.errorMessage = "Python task timed out";
209
- result.errorTraceback = "";
210
- } else {
211
- // Execute the Python work
212
- result = task.work();
213
- }
214
- } catch (const std::exception& e) {
215
- result.success = false;
216
- result.errorType = "RuntimeError";
217
- result.errorMessage = std::string("C++ exception in worker thread: ") + e.what();
218
- result.errorTraceback = "";
219
- } catch (...) {
220
- result.success = false;
221
- result.errorType = "RuntimeError";
222
- result.errorMessage = "Unknown C++ exception in worker thread";
223
- result.errorTraceback = "";
183
+ _executeTask(std::move(task));
224
184
  }
185
+ }
186
+ }
225
187
 
226
- auto endTime = std::chrono::steady_clock::now();
227
- auto durationUs = std::chrono::duration_cast<std::chrono::microseconds>(endTime - startTime).count();
228
- _stats.totalExecutionTimeUs.fetch_add(static_cast<uint64_t>(durationUs), std::memory_order_relaxed);
188
+ void PythonThreadPool::_executeTask(PythonTask task) {
189
+ _stats.activeThreads.fetch_add(1, std::memory_order_relaxed);
190
+ const auto startTime = std::chrono::steady_clock::now();
191
+ TaskResult result;
229
192
 
230
- if (result.success) {
231
- _stats.totalTasksCompleted.fetch_add(1, std::memory_order_relaxed);
193
+ try {
194
+ if (_isTimedOut(task)) {
195
+ result.success = false;
196
+ result.errorType = "TimeoutError";
197
+ result.errorMessage = "Python task timed out after " +
198
+ std::to_string(task.timeoutMs) + "ms in queue";
199
+ _stats.totalTasksTimedOut.fetch_add(1, std::memory_order_relaxed);
232
200
  } else {
233
- _stats.totalTasksFailed.fetch_add(1, std::memory_order_relaxed);
201
+ result = task.work();
234
202
  }
203
+ } catch (const std::exception& e) {
204
+ result.success = false;
205
+ result.errorType = "RuntimeError";
206
+ result.errorMessage = std::string("C++ exception in worker thread: ") + e.what();
207
+ } catch (...) {
208
+ result.success = false;
209
+ result.errorType = "RuntimeError";
210
+ result.errorMessage = "Unknown C++ exception in worker thread";
211
+ }
235
212
 
236
- _stats.activeThreads.fetch_sub(1, std::memory_order_relaxed);
213
+ const auto durationUs = std::chrono::duration_cast<std::chrono::microseconds>(
214
+ std::chrono::steady_clock::now() - startTime
215
+ ).count();
216
+ _stats.totalExecutionTimeUs.fetch_add(static_cast<uint64_t>(durationUs), std::memory_order_relaxed);
217
+ if (result.success) _stats.totalTasksCompleted.fetch_add(1, std::memory_order_relaxed);
218
+ else _stats.totalTasksFailed.fetch_add(1, std::memory_order_relaxed);
219
+ _stats.activeThreads.fetch_sub(1, std::memory_order_relaxed);
220
+ _postResult(task.id, std::move(result), std::move(task.callback));
237
221
 
238
- // Post result back to Node.js event loop
239
- _postResult(task.id, std::move(result), std::move(task.callback));
222
+ if (_draining.load()) _drainCv.notify_all();
223
+ }
240
224
 
241
- // Signal drain waiters
242
- if (_draining.load()) {
243
- std::lock_guard<std::mutex> lock(_queueMutex);
244
- _drainCv.notify_all();
225
+ void PythonThreadPool::pumpUntil(const std::atomic<bool>& completed) {
226
+ PyThreadState* state = PyEval_SaveThread();
227
+ while (!completed.load(std::memory_order_acquire) && _running.load()) {
228
+ PythonTask task;
229
+ bool hasTask = false;
230
+ {
231
+ std::unique_lock<std::mutex> lock(_queueMutex);
232
+ _queueCv.wait(lock, [this, &completed] {
233
+ return completed.load(std::memory_order_acquire) ||
234
+ !_queue.empty() || !_running.load();
235
+ });
236
+ if (!completed.load(std::memory_order_acquire) && !_queue.empty()) {
237
+ task = std::move(_queue.front());
238
+ _queue.pop_front();
239
+ _stats.currentQueueSize.fetch_sub(1, std::memory_order_relaxed);
240
+ hasTask = true;
241
+ }
242
+ }
243
+ if (hasTask) {
244
+ PyEval_RestoreThread(state);
245
+ _executeTask(std::move(task));
246
+ state = PyEval_SaveThread();
245
247
  }
246
248
  }
249
+ PyEval_RestoreThread(state);
250
+ }
251
+
252
+ void PythonThreadPool::notifyWaiters() {
253
+ _queueCv.notify_all();
247
254
  }
248
255
 
249
256
  // Result Delivery
@@ -144,10 +144,23 @@ public:
144
144
  /** Check if pool is running */
145
145
  bool isRunning() const noexcept { return _running.load(); }
146
146
 
147
+ /**
148
+ * Release the GIL and keep servicing queued Python work until a JavaScript
149
+ * callback completes. This preserves progress for reentrant calls even with
150
+ * a one-thread pool.
151
+ */
152
+ void pumpUntil(const std::atomic<bool>& completed);
153
+
154
+ /** Wake workers that may be waiting for callback completion. */
155
+ void notifyWaiters();
156
+
147
157
  private:
148
158
  /** Worker thread entry point */
149
159
  void _workerThread(uint32_t threadId);
150
160
 
161
+ /** Execute and deliver one task while the caller holds the GIL. */
162
+ void _executeTask(PythonTask task);
163
+
151
164
  /** Post result back to event loop via libuv */
152
165
  void _postResult(uint64_t taskId, TaskResult result, std::function<void(TaskResult)> callback);
153
166
 
@@ -106,14 +106,19 @@ void TypeConverter::clearRegistry() {
106
106
 
107
107
  std::string TypeConverter::getTypeName(PyObject* obj) {
108
108
  if (!obj) return "NoneType";
109
- PyObject* type = reinterpret_cast<PyObject*>(Py_TYPE(obj));
109
+ PyObject* type = PyObject_Type(obj);
110
+ if (!type) {
111
+ PyErr_Clear();
112
+ return "unknown";
113
+ }
114
+ PyRef typeRef = PyRef::steal(type);
110
115
  PyObject* name = PyObject_GetAttrString(type, "__name__");
111
116
  if (!name) {
112
117
  PyErr_Clear();
113
118
  return "unknown";
114
119
  }
115
120
  PyRef nameRef = PyRef::steal(name);
116
- const char* str = PyUnicode_AsUTF8(name);
121
+ const char* str = PyUnicode_AsUTF8AndSize(name, nullptr);
117
122
  if (!str) {
118
123
  PyErr_Clear();
119
124
  return "unknown";
@@ -123,7 +128,12 @@ std::string TypeConverter::getTypeName(PyObject* obj) {
123
128
 
124
129
  std::string TypeConverter::getQualifiedTypeName(PyObject* obj) {
125
130
  if (!obj) return "NoneType";
126
- PyObject* type = reinterpret_cast<PyObject*>(Py_TYPE(obj));
131
+ PyObject* type = PyObject_Type(obj);
132
+ if (!type) {
133
+ PyErr_Clear();
134
+ return "unknown";
135
+ }
136
+ PyRef typeRef = PyRef::steal(type);
127
137
 
128
138
  // Try __module__.__qualname__ first
129
139
  PyObject* module = PyObject_GetAttrString(type, "__module__");
@@ -132,13 +142,13 @@ std::string TypeConverter::getQualifiedTypeName(PyObject* obj) {
132
142
 
133
143
  std::string result;
134
144
  if (module && PyUnicode_Check(module)) {
135
- const char* mod = PyUnicode_AsUTF8(module);
145
+ const char* mod = PyUnicode_AsUTF8AndSize(module, nullptr);
136
146
  if (mod) {
137
147
  result = std::string(mod) + ".";
138
148
  }
139
149
  }
140
150
  if (qualname && PyUnicode_Check(qualname)) {
141
- const char* qn = PyUnicode_AsUTF8(qualname);
151
+ const char* qn = PyUnicode_AsUTF8AndSize(qualname, nullptr);
142
152
  if (qn) result += qn;
143
153
  } else {
144
154
  result += getTypeName(obj);
@@ -249,7 +259,7 @@ SerializedValue TypeConverter::pythonToSerialized(PyObject* obj, int depth) {
249
259
  if (strObj) {
250
260
  PyRef strRef = PyRef::steal(strObj);
251
261
  result.format = SerializedFormat::JSON;
252
- result.jsonData = "\"" + std::string(PyUnicode_AsUTF8(strObj)) + "\"";
262
+ result.jsonData = "\"" + std::string(PyUnicode_AsUTF8AndSize(strObj, nullptr)) + "\"";
253
263
  return result;
254
264
  }
255
265
  }
@@ -286,7 +296,7 @@ SerializedValue TypeConverter::pythonToSerialized(PyObject* obj, int depth) {
286
296
 
287
297
  // String
288
298
  if (PyUnicode_Check(obj)) {
289
- const char* str = PyUnicode_AsUTF8(obj);
299
+ const char* str = PyUnicode_AsUTF8AndSize(obj, nullptr);
290
300
  if (!str) {
291
301
  PyErr_Clear();
292
302
  result.format = SerializedFormat::JSON;
@@ -420,13 +430,13 @@ std::string TypeConverter::dictToJson(PyObject* mapping, int depth) {
420
430
  // Convert key to string
421
431
  std::string keyStr;
422
432
  if (PyUnicode_Check(key)) {
423
- const char* k = PyUnicode_AsUTF8(key);
433
+ const char* k = PyUnicode_AsUTF8AndSize(key, nullptr);
424
434
  if (k) keyStr = k;
425
435
  } else {
426
436
  PyObject* strKey = PyObject_Str(key);
427
437
  if (strKey) {
428
438
  PyRef strRef = PyRef::steal(strKey);
429
- const char* k = PyUnicode_AsUTF8(strKey);
439
+ const char* k = PyUnicode_AsUTF8AndSize(strKey, nullptr);
430
440
  if (k) keyStr = k;
431
441
  } else {
432
442
  PyErr_Clear();
@@ -470,7 +480,7 @@ SerializedValue TypeConverter::numpyArrayToBuffer(PyObject* array) {
470
480
  PyObject* dtypeName = PyObject_GetAttrString(dtypeObj, "name");
471
481
  if (dtypeName) {
472
482
  PyRef nameRef = PyRef::steal(dtypeName);
473
- const char* name = PyUnicode_AsUTF8(dtypeName);
483
+ const char* name = PyUnicode_AsUTF8AndSize(dtypeName, nullptr);
474
484
  if (name) result.metadata.dtype = name;
475
485
  } else { PyErr_Clear(); }
476
486
  } else { PyErr_Clear(); }
@@ -538,7 +548,7 @@ SerializedValue TypeConverter::dataFrameToBuffer(PyObject* df) {
538
548
  for (Py_ssize_t i = 0; i < ncols; ++i) {
539
549
  PyObject* col = PyList_GetItem(colList, i);
540
550
  if (col && PyUnicode_Check(col)) {
541
- const char* colName = PyUnicode_AsUTF8(col);
551
+ const char* colName = PyUnicode_AsUTF8AndSize(col, nullptr);
542
552
  if (colName) result.metadata.columns.push_back(colName);
543
553
  }
544
554
  }
@@ -565,7 +575,7 @@ SerializedValue TypeConverter::dataFrameToBuffer(PyObject* df) {
565
575
 
566
576
  if (jsonStr) {
567
577
  PyRef jsonRef = PyRef::steal(jsonStr);
568
- const char* json = PyUnicode_AsUTF8(jsonStr);
578
+ const char* json = PyUnicode_AsUTF8AndSize(jsonStr, nullptr);
569
579
  if (json) {
570
580
  result.jsonData = json;
571
581
  result.format = SerializedFormat::PANDAS_DATAFRAME;
@@ -588,7 +598,7 @@ SerializedValue TypeConverter::seriesToBuffer(PyObject* series) {
588
598
  PyObject* jsonStr = PyObject_CallNoArgs(toJson);
589
599
  if (jsonStr) {
590
600
  PyRef jsonRef = PyRef::steal(jsonStr);
591
- const char* json = PyUnicode_AsUTF8(jsonStr);
601
+ const char* json = PyUnicode_AsUTF8AndSize(jsonStr, nullptr);
592
602
  if (json) result.jsonData = json;
593
603
  } else { PyErr_Clear(); }
594
604
  } else { PyErr_Clear(); }
@@ -598,7 +608,7 @@ SerializedValue TypeConverter::seriesToBuffer(PyObject* series) {
598
608
  if (nameObj && nameObj != Py_None) {
599
609
  PyRef nameRef = PyRef::steal(nameObj);
600
610
  if (PyUnicode_Check(nameObj)) {
601
- const char* name = PyUnicode_AsUTF8(nameObj);
611
+ const char* name = PyUnicode_AsUTF8AndSize(nameObj, nullptr);
602
612
  if (name) result.metadata.columns.push_back(name);
603
613
  }
604
614
  } else if (nameObj) {
@@ -634,7 +644,7 @@ PyObject* TypeConverter::jsonToPython(const std::string& json) {
634
644
  }
635
645
  PyRef strRef = PyRef::steal(strObj);
636
646
 
637
- PyObject* result = PyObject_CallOneArg(loads, strObj);
647
+ PyObject* result = PyObject_CallFunctionObjArgs(loads, strObj, nullptr);
638
648
  if (!result) {
639
649
  PyErr_Clear();
640
650
  Py_RETURN_NONE;
@@ -747,7 +757,7 @@ PyObject* TypeConverter::bufferToNumpyArray(
747
757
  }
748
758
  PyRef shapeListRef = PyRef::steal(shapeList);
749
759
 
750
- PyObject* reshaped = PyObject_CallOneArg(reshape, shapeList);
760
+ PyObject* reshaped = PyObject_CallFunctionObjArgs(reshape, shapeList, nullptr);
751
761
  if (reshaped) {
752
762
  return reshaped;
753
763
  }
@@ -770,7 +780,7 @@ std::string TypeConverter::getRepr(PyObject* obj) {
770
780
  return "<error getting repr>";
771
781
  }
772
782
  PyRef reprRef = PyRef::steal(repr);
773
- const char* str = PyUnicode_AsUTF8(repr);
783
+ const char* str = PyUnicode_AsUTF8AndSize(repr, nullptr);
774
784
  if (!str) {
775
785
  PyErr_Clear();
776
786
  return "<encoding error>";
@@ -786,7 +796,7 @@ std::string TypeConverter::getStr(PyObject* obj) {
786
796
  return "<error>";
787
797
  }
788
798
  PyRef strRef = PyRef::steal(str);
789
- const char* cstr = PyUnicode_AsUTF8(str);
799
+ const char* cstr = PyUnicode_AsUTF8AndSize(str, nullptr);
790
800
  if (!cstr) {
791
801
  PyErr_Clear();
792
802
  return "";
@@ -815,7 +825,7 @@ std::tuple<std::string, std::string, std::string> TypeConverter::captureExceptio
815
825
  PyObject* name = PyObject_GetAttrString(excType, "__name__");
816
826
  if (name) {
817
827
  PyRef nameRef = PyRef::steal(name);
818
- const char* n = PyUnicode_AsUTF8(name);
828
+ const char* n = PyUnicode_AsUTF8AndSize(name, nullptr);
819
829
  if (n) typeName = n;
820
830
  } else { PyErr_Clear(); }
821
831
  }
@@ -825,7 +835,7 @@ std::tuple<std::string, std::string, std::string> TypeConverter::captureExceptio
825
835
  PyObject* str = PyObject_Str(excValue);
826
836
  if (str) {
827
837
  PyRef strRef = PyRef::steal(str);
828
- const char* s = PyUnicode_AsUTF8(str);
838
+ const char* s = PyUnicode_AsUTF8AndSize(str, nullptr);
829
839
  if (s) message = s;
830
840
  } else { PyErr_Clear(); }
831
841
  }
@@ -838,7 +848,7 @@ std::tuple<std::string, std::string, std::string> TypeConverter::captureExceptio
838
848
  PyObject* formatTb = PyObject_GetAttrString(tbModule, "format_tb");
839
849
  if (formatTb) {
840
850
  PyRef formatTbRef = PyRef::steal(formatTb);
841
- PyObject* tbLines = PyObject_CallOneArg(formatTb, excTb);
851
+ PyObject* tbLines = PyObject_CallFunctionObjArgs(formatTb, excTb, nullptr);
842
852
  if (tbLines) {
843
853
  PyRef tbLinesRef = PyRef::steal(tbLines);
844
854
  std::ostringstream oss;
@@ -847,7 +857,7 @@ std::tuple<std::string, std::string, std::string> TypeConverter::captureExceptio
847
857
  for (Py_ssize_t i = 0; i < nlines; ++i) {
848
858
  PyObject* line = PyList_GetItem(tbLines, i);
849
859
  if (line && PyUnicode_Check(line)) {
850
- const char* l = PyUnicode_AsUTF8(line);
860
+ const char* l = PyUnicode_AsUTF8AndSize(line, nullptr);
851
861
  if (l) oss << l;
852
862
  }
853
863
  }