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.
- package/README.md +50 -4
- package/binding.gyp +2 -1
- package/dist/cli/codegen.d.ts +3 -0
- package/dist/cli/codegen.d.ts.map +1 -1
- package/dist/cli/codegen.js +12 -0
- package/dist/cli/codegen.js.map +1 -1
- package/dist/config/project.d.ts +7 -0
- package/dist/config/project.d.ts.map +1 -1
- package/dist/config/project.js +23 -2
- package/dist/config/project.js.map +1 -1
- package/dist/core/py-proxy.d.ts +1 -1
- package/dist/core/py-proxy.d.ts.map +1 -1
- package/dist/core/py-proxy.js +40 -7
- package/dist/core/py-proxy.js.map +1 -1
- package/dist/core/py-runtime.d.ts +7 -0
- package/dist/core/py-runtime.d.ts.map +1 -1
- package/dist/core/py-runtime.js +50 -3
- package/dist/core/py-runtime.js.map +1 -1
- package/dist/core/pynect-callback.d.ts +25 -0
- package/dist/core/pynect-callback.d.ts.map +1 -0
- package/dist/core/pynect-callback.js +99 -0
- package/dist/core/pynect-callback.js.map +1 -0
- package/dist/env/project-environment.d.ts.map +1 -1
- package/dist/env/project-environment.js +1 -0
- package/dist/env/project-environment.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/native/native-addon-loader.d.ts.map +1 -1
- package/dist/native/native-addon-loader.js +34 -10
- package/dist/native/native-addon-loader.js.map +1 -1
- package/dist/types/addon.d.ts +10 -0
- package/dist/types/addon.d.ts.map +1 -1
- package/dist/types/addon.js.map +1 -1
- package/dist/types/config.d.ts +10 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +6 -0
- package/dist/types/config.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/python.d.ts +2 -0
- package/dist/types/python.d.ts.map +1 -1
- package/package.json +2 -2
- package/prebuilds/darwin-arm64/pynect-prebuild.json +5 -4
- package/prebuilds/darwin-arm64/pynect_addon.node +0 -0
- package/prebuilds/darwin-x64/pynect-prebuild.json +5 -4
- package/prebuilds/darwin-x64/pynect_addon.node +0 -0
- package/prebuilds/linux-arm64/pynect-prebuild.json +5 -4
- package/prebuilds/linux-arm64/pynect_addon.node +0 -0
- package/prebuilds/linux-x64/pynect-prebuild.json +5 -4
- package/prebuilds/linux-x64/pynect_addon.node +0 -0
- package/prebuilds/win32-arm64/pynect-prebuild.json +5 -4
- package/prebuilds/win32-arm64/pynect_addon.node +0 -0
- package/prebuilds/win32-x64/pynect-prebuild.json +5 -4
- package/prebuilds/win32-x64/pynect_addon.node +0 -0
- package/python/type_inspector.py +12 -0
- package/scripts/build-native.js +9 -3
- package/scripts/ensure-native.js +8 -2
- package/scripts/smoke-python-boundary.js +71 -1
- package/src/native/callback_bridge.h +11 -0
- package/src/native/native-addon-loader.ts +39 -11
- package/src/native/pynect_addon.cpp +354 -4
- package/src/native/python_bridge.cpp +117 -40
- package/src/native/python_bridge.h +13 -0
- package/src/native/thread_pool.cpp +62 -55
- package/src/native/thread_pool.h +13 -0
- package/src/native/type_converter.cpp +32 -22
- 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
|
-
|
|
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
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
//
|
|
118
|
-
|
|
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) && !
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
675
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
222
|
+
if (_draining.load()) _drainCv.notify_all();
|
|
223
|
+
}
|
|
240
224
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
package/src/native/thread_pool.h
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
860
|
+
const char* l = PyUnicode_AsUTF8AndSize(line, nullptr);
|
|
851
861
|
if (l) oss << l;
|
|
852
862
|
}
|
|
853
863
|
}
|