pynect 0.2.7 → 0.2.10
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 +68 -27
- package/SECURITY.md +6 -6
- package/dist/cli/codegen.d.ts +3 -0
- package/dist/cli/codegen.d.ts.map +1 -1
- package/dist/cli/codegen.js +15 -3
- package/dist/cli/codegen.js.map +1 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +38 -19
- package/dist/cli/sync.js.map +1 -1
- package/dist/config/project.d.ts +8 -52
- package/dist/config/project.d.ts.map +1 -1
- package/dist/config/project.js +74 -121
- package/dist/config/project.js.map +1 -1
- package/dist/core/isolated-worker.js +4 -4
- package/dist/core/isolated-worker.js.map +1 -1
- package/dist/core/py-proxy.d.ts +2 -1
- package/dist/core/py-proxy.d.ts.map +1 -1
- package/dist/core/py-proxy.js +55 -7
- package/dist/core/py-proxy.js.map +1 -1
- package/dist/core/py-runtime.d.ts +1 -5
- package/dist/core/py-runtime.d.ts.map +1 -1
- package/dist/core/py-runtime.js +36 -100
- package/dist/core/py-runtime.js.map +1 -1
- package/dist/core/pynect-callback.d.ts +16 -0
- package/dist/core/pynect-callback.d.ts.map +1 -0
- package/dist/core/pynect-callback.js +52 -0
- package/dist/core/pynect-callback.js.map +1 -0
- package/dist/env/project-environment.d.ts +9 -30
- package/dist/env/project-environment.d.ts.map +1 -1
- package/dist/env/project-environment.js +32 -217
- package/dist/env/project-environment.js.map +1 -1
- package/dist/env/python.d.ts +16 -0
- package/dist/env/python.d.ts.map +1 -0
- package/dist/env/python.js +9 -0
- package/dist/env/python.js.map +1 -0
- package/dist/index.d.ts +8 -24
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -31
- 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 +15 -5
- package/dist/native/native-addon-loader.js.map +1 -1
- package/dist/types/addon.d.ts +6 -0
- package/dist/types/addon.d.ts.map +1 -1
- package/dist/types/addon.js.map +1 -1
- package/dist/types/config.d.ts +18 -85
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +17 -15
- 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/python.d.ts +2 -0
- package/dist/types/python.d.ts.map +1 -1
- package/package.json +3 -4
- package/prebuilds/darwin-arm64/pynect-prebuild.json +1 -1
- package/prebuilds/darwin-arm64/pynect_addon.node +0 -0
- package/prebuilds/darwin-x64/pynect-prebuild.json +1 -1
- package/prebuilds/darwin-x64/pynect_addon.node +0 -0
- package/prebuilds/linux-arm64/pynect-prebuild.json +1 -1
- package/prebuilds/linux-arm64/pynect_addon.node +0 -0
- package/prebuilds/linux-x64/pynect-prebuild.json +1 -1
- package/prebuilds/linux-x64/pynect_addon.node +0 -0
- package/prebuilds/win32-arm64/pynect-prebuild.json +1 -1
- package/prebuilds/win32-arm64/pynect_addon.node +0 -0
- package/prebuilds/win32-x64/pynect-prebuild.json +1 -1
- package/prebuilds/win32-x64/pynect_addon.node +0 -0
- package/python/pynect_runtime.py +1 -1
- package/python/type_inspector.py +12 -0
- package/scripts/build-native.js +4 -3
- package/scripts/postinstall.js +2 -2
- package/scripts/python.js +33 -0
- package/scripts/smoke-python-boundary.js +72 -21
- package/src/native/callback_bridge.h +11 -0
- package/src/native/native-addon-loader.ts +13 -4
- package/src/native/pynect_addon.cpp +346 -4
- package/src/native/python_bridge.cpp +12 -0
- package/src/native/python_bridge.h +7 -0
- package/src/native/thread_pool.cpp +62 -55
- package/src/native/thread_pool.h +13 -0
- package/src/native/type_converter.h +2 -0
- package/dist/env/executables.d.ts +0 -3
- package/dist/env/executables.d.ts.map +0 -1
- package/dist/env/executables.js +0 -14
- package/dist/env/executables.js.map +0 -1
- package/dist/env/package-installer.d.ts +0 -232
- package/dist/env/package-installer.d.ts.map +0 -1
- package/dist/env/package-installer.js +0 -615
- package/dist/env/package-installer.js.map +0 -1
- package/dist/env/pyfig-adapter.d.ts +0 -38
- package/dist/env/pyfig-adapter.d.ts.map +0 -1
- package/dist/env/pyfig-adapter.js +0 -10
- package/dist/env/pyfig-adapter.js.map +0 -1
- package/dist/env/python-detector.d.ts +0 -48
- package/dist/env/python-detector.d.ts.map +0 -1
- package/dist/env/python-detector.js +0 -182
- package/dist/env/python-detector.js.map +0 -1
- package/dist/env/python-provisioner.d.ts +0 -16
- package/dist/env/python-provisioner.d.ts.map +0 -1
- package/dist/env/python-provisioner.js +0 -42
- package/dist/env/python-provisioner.js.map +0 -1
- package/dist/env/venv-manager.d.ts +0 -56
- package/dist/env/venv-manager.d.ts.map +0 -1
- package/dist/env/venv-manager.js +0 -160
- package/dist/env/venv-manager.js.map +0 -1
- package/scripts/pyfig.js +0 -142
- package/scripts/python-provisioner.js +0 -23
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
#include "thread_pool.h"
|
|
20
20
|
#include "type_converter.h"
|
|
21
21
|
#include "gil_guard.h"
|
|
22
|
+
#include "callback_bridge.h"
|
|
22
23
|
|
|
23
24
|
#include <memory>
|
|
24
25
|
#include <string>
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
#include <iostream>
|
|
30
31
|
#include <chrono>
|
|
31
32
|
#include <ctime>
|
|
33
|
+
#include <unordered_map>
|
|
32
34
|
|
|
33
35
|
namespace pynect {
|
|
34
36
|
|
|
@@ -37,6 +39,38 @@ namespace pynect {
|
|
|
37
39
|
static std::unique_ptr<PythonThreadPool> g_threadPool;
|
|
38
40
|
static bool g_initialized = false;
|
|
39
41
|
static bool g_includeTracebacks = false;
|
|
42
|
+
static uint32_t g_callbackMaxDepth = 0;
|
|
43
|
+
static std::atomic<uint32_t> g_nextCallbackId{1};
|
|
44
|
+
static std::mutex g_callbacksMutex;
|
|
45
|
+
static thread_local uint32_t g_callbackDepth = 0;
|
|
46
|
+
|
|
47
|
+
struct CallbackState {
|
|
48
|
+
explicit CallbackState(Napi::ThreadSafeFunction callbackDispatcher)
|
|
49
|
+
: dispatcher(std::move(callbackDispatcher)) {}
|
|
50
|
+
|
|
51
|
+
Napi::ThreadSafeFunction dispatcher;
|
|
52
|
+
std::atomic<bool> active{true};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
struct CallbackCompletion {
|
|
56
|
+
std::atomic<bool> completed{false};
|
|
57
|
+
std::mutex mutex;
|
|
58
|
+
SerializedValue value;
|
|
59
|
+
bool failed = false;
|
|
60
|
+
std::string errorName;
|
|
61
|
+
std::string errorMessage;
|
|
62
|
+
std::string errorStack;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
struct CallbackInvocationPayload {
|
|
66
|
+
std::shared_ptr<CallbackState> state;
|
|
67
|
+
std::shared_ptr<CallbackCompletion> completion;
|
|
68
|
+
uint32_t depth;
|
|
69
|
+
std::vector<TaskResult> args;
|
|
70
|
+
std::map<std::string, TaskResult> kwargs;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
static std::unordered_map<uint32_t, std::shared_ptr<CallbackState>> g_callbacks;
|
|
40
74
|
|
|
41
75
|
// Helper: Extract SerializedValue from JS object
|
|
42
76
|
|
|
@@ -58,6 +92,15 @@ static SerializedValue jsToSerializedValue(const Napi::Object& obj) {
|
|
|
58
92
|
} else if (data.IsBuffer()) {
|
|
59
93
|
Napi::Buffer<uint8_t> buf = data.As<Napi::Buffer<uint8_t>>();
|
|
60
94
|
sv.binaryData.assign(buf.Data(), buf.Data() + buf.Length());
|
|
95
|
+
} else if (data.IsTypedArray()) {
|
|
96
|
+
Napi::TypedArray array = data.As<Napi::TypedArray>();
|
|
97
|
+
Napi::ArrayBuffer buffer = array.ArrayBuffer();
|
|
98
|
+
auto* begin = static_cast<uint8_t*>(buffer.Data()) + array.ByteOffset();
|
|
99
|
+
sv.binaryData.assign(begin, begin + array.ByteLength());
|
|
100
|
+
} else if (data.IsArrayBuffer()) {
|
|
101
|
+
Napi::ArrayBuffer buffer = data.As<Napi::ArrayBuffer>();
|
|
102
|
+
auto* begin = static_cast<uint8_t*>(buffer.Data());
|
|
103
|
+
sv.binaryData.assign(begin, begin + buffer.ByteLength());
|
|
61
104
|
}
|
|
62
105
|
}
|
|
63
106
|
|
|
@@ -66,6 +109,9 @@ static SerializedValue jsToSerializedValue(const Napi::Object& obj) {
|
|
|
66
109
|
if (meta.Has("objectId")) {
|
|
67
110
|
sv.metadata.objectId = meta.Get("objectId").As<Napi::Number>().Uint32Value();
|
|
68
111
|
}
|
|
112
|
+
if (meta.Has("callbackId")) {
|
|
113
|
+
sv.metadata.callbackId = meta.Get("callbackId").As<Napi::Number>().Uint32Value();
|
|
114
|
+
}
|
|
69
115
|
if (meta.Has("dtype") && meta.Get("dtype").IsString()) {
|
|
70
116
|
sv.metadata.dtype = meta.Get("dtype").As<Napi::String>().Utf8Value();
|
|
71
117
|
}
|
|
@@ -115,6 +161,230 @@ static Napi::Object taskResultToJS(Napi::Env env, const TaskResult& result) {
|
|
|
115
161
|
return obj;
|
|
116
162
|
}
|
|
117
163
|
|
|
164
|
+
static void completeCallback(
|
|
165
|
+
const std::shared_ptr<CallbackCompletion>& completion,
|
|
166
|
+
SerializedValue value
|
|
167
|
+
) {
|
|
168
|
+
{
|
|
169
|
+
std::lock_guard<std::mutex> lock(completion->mutex);
|
|
170
|
+
if (completion->completed.load(std::memory_order_relaxed)) return;
|
|
171
|
+
completion->value = std::move(value);
|
|
172
|
+
completion->completed.store(true, std::memory_order_release);
|
|
173
|
+
}
|
|
174
|
+
if (g_threadPool) g_threadPool->notifyWaiters();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
static void failCallback(
|
|
178
|
+
const std::shared_ptr<CallbackCompletion>& completion,
|
|
179
|
+
std::string name,
|
|
180
|
+
std::string message,
|
|
181
|
+
std::string stack = {}
|
|
182
|
+
) {
|
|
183
|
+
{
|
|
184
|
+
std::lock_guard<std::mutex> lock(completion->mutex);
|
|
185
|
+
if (completion->completed.load(std::memory_order_relaxed)) return;
|
|
186
|
+
completion->failed = true;
|
|
187
|
+
completion->errorName = std::move(name);
|
|
188
|
+
completion->errorMessage = std::move(message);
|
|
189
|
+
completion->errorStack = std::move(stack);
|
|
190
|
+
completion->completed.store(true, std::memory_order_release);
|
|
191
|
+
}
|
|
192
|
+
if (g_threadPool) g_threadPool->notifyWaiters();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
static std::shared_ptr<CallbackState> callbackState(uint32_t callbackId) {
|
|
196
|
+
std::lock_guard<std::mutex> lock(g_callbacksMutex);
|
|
197
|
+
auto found = g_callbacks.find(callbackId);
|
|
198
|
+
if (found == g_callbacks.end() || !found->second->active.load()) return nullptr;
|
|
199
|
+
return found->second;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
struct CallbackDepthScope {
|
|
203
|
+
explicit CallbackDepthScope(uint32_t depth)
|
|
204
|
+
: previous(g_callbackDepth) {
|
|
205
|
+
g_callbackDepth = depth;
|
|
206
|
+
}
|
|
207
|
+
~CallbackDepthScope() { g_callbackDepth = previous; }
|
|
208
|
+
uint32_t previous;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
static constexpr const char* CALLBACK_CAPSULE = "pynect.callback";
|
|
212
|
+
|
|
213
|
+
static void destroyCallbackCapsule(PyObject* capsule) {
|
|
214
|
+
auto* callbackId = static_cast<uint32_t*>(PyCapsule_GetPointer(capsule, CALLBACK_CAPSULE));
|
|
215
|
+
if (!callbackId) PyErr_Clear();
|
|
216
|
+
delete callbackId;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
static PyObject* invokeJavaScriptCallback(
|
|
220
|
+
PyObject* capsule,
|
|
221
|
+
PyObject* args,
|
|
222
|
+
PyObject* kwargs
|
|
223
|
+
) {
|
|
224
|
+
auto* callbackId = static_cast<uint32_t*>(PyCapsule_GetPointer(capsule, CALLBACK_CAPSULE));
|
|
225
|
+
if (!callbackId) return nullptr;
|
|
226
|
+
auto state = callbackState(*callbackId);
|
|
227
|
+
if (!state) {
|
|
228
|
+
PyErr_SetString(PyExc_RuntimeError, "Pynect callback has been disposed");
|
|
229
|
+
return nullptr;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const uint32_t depth = g_callbackDepth + 1;
|
|
233
|
+
if (g_callbackMaxDepth > 0 && depth > g_callbackMaxDepth) {
|
|
234
|
+
PyErr_Format(
|
|
235
|
+
PyExc_RecursionError,
|
|
236
|
+
"Pynect callback recursion depth %u exceeds the configured maximum of %u",
|
|
237
|
+
depth,
|
|
238
|
+
g_callbackMaxDepth
|
|
239
|
+
);
|
|
240
|
+
return nullptr;
|
|
241
|
+
}
|
|
242
|
+
CallbackDepthScope depthScope(depth);
|
|
243
|
+
|
|
244
|
+
auto completion = std::make_shared<CallbackCompletion>();
|
|
245
|
+
auto* payload = new CallbackInvocationPayload{state, completion, depth, {}, {}};
|
|
246
|
+
const Py_ssize_t argCount = PyTuple_Size(args);
|
|
247
|
+
payload->args.reserve(static_cast<size_t>(argCount));
|
|
248
|
+
for (Py_ssize_t index = 0; index < argCount; ++index) {
|
|
249
|
+
PyObject* value = PyTuple_GetItem(args, index);
|
|
250
|
+
payload->args.push_back(PythonBridge::getInstance().serializeValue(value));
|
|
251
|
+
}
|
|
252
|
+
if (kwargs) {
|
|
253
|
+
PyObject* key = nullptr;
|
|
254
|
+
PyObject* value = nullptr;
|
|
255
|
+
Py_ssize_t position = 0;
|
|
256
|
+
while (PyDict_Next(kwargs, &position, &key, &value)) {
|
|
257
|
+
Py_ssize_t length = 0;
|
|
258
|
+
const char* text = PyUnicode_AsUTF8AndSize(key, &length);
|
|
259
|
+
if (!text) {
|
|
260
|
+
delete payload;
|
|
261
|
+
return nullptr;
|
|
262
|
+
}
|
|
263
|
+
payload->kwargs.emplace(
|
|
264
|
+
std::string(text, static_cast<size_t>(length)),
|
|
265
|
+
PythonBridge::getInstance().serializeValue(value)
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const napi_status status = state->dispatcher.NonBlockingCall(
|
|
271
|
+
payload,
|
|
272
|
+
[](Napi::Env env, Napi::Function dispatcher, CallbackInvocationPayload* invocation) {
|
|
273
|
+
const auto completion = invocation->completion;
|
|
274
|
+
try {
|
|
275
|
+
Napi::Object request = Napi::Object::New(env);
|
|
276
|
+
request.Set("depth", Napi::Number::New(env, invocation->depth));
|
|
277
|
+
Napi::Array argsArray = Napi::Array::New(env, invocation->args.size());
|
|
278
|
+
for (size_t index = 0; index < invocation->args.size(); ++index) {
|
|
279
|
+
argsArray.Set(index, taskResultToJS(env, invocation->args[index]));
|
|
280
|
+
}
|
|
281
|
+
request.Set("args", argsArray);
|
|
282
|
+
Napi::Object kwargsObject = Napi::Object::New(env);
|
|
283
|
+
for (const auto& [key, value] : invocation->kwargs) {
|
|
284
|
+
kwargsObject.Set(key, taskResultToJS(env, value));
|
|
285
|
+
}
|
|
286
|
+
request.Set("kwargs", kwargsObject);
|
|
287
|
+
request.Set("resolve", Napi::Function::New(
|
|
288
|
+
env,
|
|
289
|
+
[completion](const Napi::CallbackInfo& info) {
|
|
290
|
+
try {
|
|
291
|
+
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
292
|
+
failCallback(completion, "TypeError", "Pynect callback returned an invalid value");
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
completeCallback(completion, jsToSerializedValue(info[0].As<Napi::Object>()));
|
|
296
|
+
} catch (const std::exception& error) {
|
|
297
|
+
failCallback(completion, "TypeError", error.what());
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
));
|
|
301
|
+
request.Set("reject", Napi::Function::New(
|
|
302
|
+
env,
|
|
303
|
+
[completion](const Napi::CallbackInfo& info) {
|
|
304
|
+
std::string name = "Error";
|
|
305
|
+
std::string message = "JavaScript callback failed";
|
|
306
|
+
std::string stack;
|
|
307
|
+
if (info.Length() > 0 && info[0].IsObject()) {
|
|
308
|
+
Napi::Object error = info[0].As<Napi::Object>();
|
|
309
|
+
if (error.Has("name")) name = error.Get("name").ToString().Utf8Value();
|
|
310
|
+
if (error.Has("message")) message = error.Get("message").ToString().Utf8Value();
|
|
311
|
+
if (error.Has("stack")) stack = error.Get("stack").ToString().Utf8Value();
|
|
312
|
+
}
|
|
313
|
+
failCallback(completion, std::move(name), std::move(message), std::move(stack));
|
|
314
|
+
}
|
|
315
|
+
));
|
|
316
|
+
dispatcher.Call({request});
|
|
317
|
+
} catch (const std::exception& error) {
|
|
318
|
+
failCallback(completion, "Error", error.what());
|
|
319
|
+
}
|
|
320
|
+
delete invocation;
|
|
321
|
+
}
|
|
322
|
+
);
|
|
323
|
+
if (status != napi_ok) {
|
|
324
|
+
delete payload;
|
|
325
|
+
PyErr_SetString(PyExc_RuntimeError, "Unable to schedule the JavaScript callback");
|
|
326
|
+
return nullptr;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!g_threadPool) {
|
|
330
|
+
failCallback(completion, "RuntimeError", "Pynect thread pool is unavailable");
|
|
331
|
+
} else {
|
|
332
|
+
g_threadPool->pumpUntil(completion->completed);
|
|
333
|
+
}
|
|
334
|
+
if (!completion->completed.load(std::memory_order_acquire)) {
|
|
335
|
+
PyErr_SetString(PyExc_RuntimeError, "Pynect stopped while a callback was running");
|
|
336
|
+
return nullptr;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
std::lock_guard<std::mutex> lock(completion->mutex);
|
|
340
|
+
if (completion->failed) {
|
|
341
|
+
std::string message = completion->errorName + ": " + completion->errorMessage;
|
|
342
|
+
if (g_includeTracebacks && !completion->errorStack.empty()) {
|
|
343
|
+
message += "\n" + completion->errorStack;
|
|
344
|
+
}
|
|
345
|
+
PyErr_SetString(PyExc_RuntimeError, message.c_str());
|
|
346
|
+
return nullptr;
|
|
347
|
+
}
|
|
348
|
+
return PythonBridge::getInstance().deserializeValue(completion->value);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
PyObject* createPythonCallback(uint32_t callbackId) {
|
|
352
|
+
if (!callbackState(callbackId)) {
|
|
353
|
+
PyErr_SetString(PyExc_RuntimeError, "Pynect callback is not registered");
|
|
354
|
+
return nullptr;
|
|
355
|
+
}
|
|
356
|
+
auto* storedId = new uint32_t(callbackId);
|
|
357
|
+
PyObject* capsule = PyCapsule_New(storedId, CALLBACK_CAPSULE, destroyCallbackCapsule);
|
|
358
|
+
if (!capsule) {
|
|
359
|
+
delete storedId;
|
|
360
|
+
return nullptr;
|
|
361
|
+
}
|
|
362
|
+
PyRef capsuleRef = PyRef::steal(capsule);
|
|
363
|
+
static PyMethodDef method = {
|
|
364
|
+
"pynect_callback",
|
|
365
|
+
reinterpret_cast<PyCFunction>(invokeJavaScriptCallback),
|
|
366
|
+
METH_VARARGS | METH_KEYWORDS,
|
|
367
|
+
"Invoke a JavaScript callback registered with Pynect."
|
|
368
|
+
};
|
|
369
|
+
return PyCFunction_NewEx(&method, capsule, nullptr);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
static uint32_t pythonRecursionLimit() {
|
|
373
|
+
GILAcquire gil;
|
|
374
|
+
PyObject* sys = PyImport_ImportModule("sys");
|
|
375
|
+
if (!sys) throw std::runtime_error("Unable to import sys for the recursion limit");
|
|
376
|
+
PyRef sysRef = PyRef::steal(sys);
|
|
377
|
+
PyObject* getter = PyObject_GetAttrString(sys, "getrecursionlimit");
|
|
378
|
+
if (!getter) throw std::runtime_error("sys.getrecursionlimit is unavailable");
|
|
379
|
+
PyRef getterRef = PyRef::steal(getter);
|
|
380
|
+
PyObject* value = PyObject_CallNoArgs(getter);
|
|
381
|
+
if (!value) throw std::runtime_error("Unable to read Python's recursion limit");
|
|
382
|
+
PyRef valueRef = PyRef::steal(value);
|
|
383
|
+
const long limit = PyLong_AsLong(value);
|
|
384
|
+
if (limit <= 0) throw std::runtime_error("Python returned an invalid recursion limit");
|
|
385
|
+
return static_cast<uint32_t>(limit);
|
|
386
|
+
}
|
|
387
|
+
|
|
118
388
|
// Exported Functions
|
|
119
389
|
|
|
120
390
|
/**
|
|
@@ -156,6 +426,9 @@ static Napi::Value InitializePython(const Napi::CallbackInfo& info) {
|
|
|
156
426
|
uint32_t maxQueueSize = opts.Has("maxQueueSize") ?
|
|
157
427
|
opts.Get("maxQueueSize").As<Napi::Number>().Uint32Value() : 1000;
|
|
158
428
|
|
|
429
|
+
uint32_t callbackMaxDepth = opts.Has("callbackMaxDepth") ?
|
|
430
|
+
opts.Get("callbackMaxDepth").As<Napi::Number>().Uint32Value() : 0;
|
|
431
|
+
|
|
159
432
|
g_includeTracebacks = opts.Has("includeTracebacks") &&
|
|
160
433
|
opts.Get("includeTracebacks").ToBoolean().Value();
|
|
161
434
|
|
|
@@ -192,6 +465,7 @@ static Napi::Value InitializePython(const Napi::CallbackInfo& info) {
|
|
|
192
465
|
pythonPathExtra,
|
|
193
466
|
envVars
|
|
194
467
|
);
|
|
468
|
+
g_callbackMaxDepth = callbackMaxDepth > 0 ? callbackMaxDepth : pythonRecursionLimit();
|
|
195
469
|
|
|
196
470
|
// Initialize thread pool
|
|
197
471
|
g_threadPool = std::make_unique<PythonThreadPool>(threadPoolSize, maxQueueSize);
|
|
@@ -225,6 +499,16 @@ static Napi::Value FinalizePython(const Napi::CallbackInfo& info) {
|
|
|
225
499
|
"Cannot finalize Python while calls are active or queued"
|
|
226
500
|
);
|
|
227
501
|
}
|
|
502
|
+
std::vector<std::shared_ptr<CallbackState>> callbacks;
|
|
503
|
+
{
|
|
504
|
+
std::lock_guard<std::mutex> lock(g_callbacksMutex);
|
|
505
|
+
for (auto& [id, callback] : g_callbacks) {
|
|
506
|
+
callback->active.store(false);
|
|
507
|
+
callbacks.push_back(callback);
|
|
508
|
+
}
|
|
509
|
+
g_callbacks.clear();
|
|
510
|
+
}
|
|
511
|
+
for (auto& callback : callbacks) callback->dispatcher.Release();
|
|
228
512
|
if (g_threadPool) {
|
|
229
513
|
g_threadPool->shutdown();
|
|
230
514
|
g_threadPool.reset();
|
|
@@ -247,6 +531,51 @@ static Napi::Value IsInitialized(const Napi::CallbackInfo& info) {
|
|
|
247
531
|
return Napi::Boolean::New(info.Env(), g_initialized);
|
|
248
532
|
}
|
|
249
533
|
|
|
534
|
+
static Napi::Value RegisterCallback(const Napi::CallbackInfo& info) {
|
|
535
|
+
Napi::Env env = info.Env();
|
|
536
|
+
if (!g_initialized || info.Length() < 1 || !info[0].IsFunction()) {
|
|
537
|
+
Napi::TypeError::New(env, "registerCallback expects a function on an initialized runtime")
|
|
538
|
+
.ThrowAsJavaScriptException();
|
|
539
|
+
return env.Undefined();
|
|
540
|
+
}
|
|
541
|
+
const uint32_t callbackId = g_nextCallbackId.fetch_add(1, std::memory_order_relaxed);
|
|
542
|
+
Napi::ThreadSafeFunction dispatcher = Napi::ThreadSafeFunction::New(
|
|
543
|
+
env,
|
|
544
|
+
info[0].As<Napi::Function>(),
|
|
545
|
+
"PynectJavaScriptCallback",
|
|
546
|
+
0,
|
|
547
|
+
1
|
|
548
|
+
);
|
|
549
|
+
dispatcher.Unref(env);
|
|
550
|
+
auto state = std::make_shared<CallbackState>(std::move(dispatcher));
|
|
551
|
+
{
|
|
552
|
+
std::lock_guard<std::mutex> lock(g_callbacksMutex);
|
|
553
|
+
g_callbacks.emplace(callbackId, state);
|
|
554
|
+
}
|
|
555
|
+
return Napi::Number::New(env, callbackId);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
static Napi::Value DisposeCallback(const Napi::CallbackInfo& info) {
|
|
559
|
+
Napi::Env env = info.Env();
|
|
560
|
+
if (info.Length() < 1 || !info[0].IsNumber()) {
|
|
561
|
+
Napi::TypeError::New(env, "disposeCallback expects a callback ID")
|
|
562
|
+
.ThrowAsJavaScriptException();
|
|
563
|
+
return env.Undefined();
|
|
564
|
+
}
|
|
565
|
+
const uint32_t callbackId = info[0].As<Napi::Number>().Uint32Value();
|
|
566
|
+
std::shared_ptr<CallbackState> state;
|
|
567
|
+
{
|
|
568
|
+
std::lock_guard<std::mutex> lock(g_callbacksMutex);
|
|
569
|
+
auto found = g_callbacks.find(callbackId);
|
|
570
|
+
if (found != g_callbacks.end()) {
|
|
571
|
+
state = found->second;
|
|
572
|
+
g_callbacks.erase(found);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (state && state->active.exchange(false)) state->dispatcher.Release();
|
|
576
|
+
return env.Undefined();
|
|
577
|
+
}
|
|
578
|
+
|
|
250
579
|
// Generic async wrapper
|
|
251
580
|
|
|
252
581
|
/**
|
|
@@ -257,7 +586,8 @@ static Napi::Value SubmitTask(
|
|
|
257
586
|
Napi::Env env,
|
|
258
587
|
std::function<TaskResult()> work,
|
|
259
588
|
uint32_t timeoutMs = 0,
|
|
260
|
-
std::string cancelKey = {}
|
|
589
|
+
std::string cancelKey = {},
|
|
590
|
+
uint32_t callbackDepth = 0
|
|
261
591
|
) {
|
|
262
592
|
auto deferred = std::make_shared<Napi::Promise::Deferred>(
|
|
263
593
|
Napi::Promise::Deferred::New(env)
|
|
@@ -269,8 +599,12 @@ static Napi::Value SubmitTask(
|
|
|
269
599
|
}
|
|
270
600
|
|
|
271
601
|
// Capture deferred by shared_ptr to safely use in callback
|
|
602
|
+
auto scopedWork = [work = std::move(work), callbackDepth]() mutable -> TaskResult {
|
|
603
|
+
CallbackDepthScope scope(callbackDepth);
|
|
604
|
+
return work();
|
|
605
|
+
};
|
|
272
606
|
g_threadPool->submit(
|
|
273
|
-
std::move(
|
|
607
|
+
std::move(scopedWork),
|
|
274
608
|
[deferred](TaskResult result) {
|
|
275
609
|
Napi::Env callEnv = deferred->Env();
|
|
276
610
|
Napi::HandleScope scope(callEnv);
|
|
@@ -481,11 +815,14 @@ static Napi::Value CallFunction(const Napi::CallbackInfo& info) {
|
|
|
481
815
|
if (optsObj.Has("requestId") && optsObj.Get("requestId").IsString()) {
|
|
482
816
|
options.requestId = optsObj.Get("requestId").As<Napi::String>().Utf8Value();
|
|
483
817
|
}
|
|
818
|
+
if (optsObj.Has("callbackDepth") && optsObj.Get("callbackDepth").IsNumber()) {
|
|
819
|
+
options.callbackDepth = optsObj.Get("callbackDepth").As<Napi::Number>().Uint32Value();
|
|
820
|
+
}
|
|
484
821
|
}
|
|
485
822
|
|
|
486
823
|
return SubmitTask(env, [objectId, path, options]() -> TaskResult {
|
|
487
824
|
return PythonBridge::getInstance().callFunctionSync(objectId, path, options);
|
|
488
|
-
}, options.timeoutMs, options.requestId);
|
|
825
|
+
}, options.timeoutMs, options.requestId, options.callbackDepth);
|
|
489
826
|
}
|
|
490
827
|
|
|
491
828
|
/**
|
|
@@ -516,11 +853,14 @@ static Napi::Value CallMethod(const Napi::CallbackInfo& info) {
|
|
|
516
853
|
if (optsObj.Has("requestId") && optsObj.Get("requestId").IsString()) {
|
|
517
854
|
options.requestId = optsObj.Get("requestId").As<Napi::String>().Utf8Value();
|
|
518
855
|
}
|
|
856
|
+
if (optsObj.Has("callbackDepth") && optsObj.Get("callbackDepth").IsNumber()) {
|
|
857
|
+
options.callbackDepth = optsObj.Get("callbackDepth").As<Napi::Number>().Uint32Value();
|
|
858
|
+
}
|
|
519
859
|
}
|
|
520
860
|
|
|
521
861
|
return SubmitTask(env, [objectId, methodName, options]() -> TaskResult {
|
|
522
862
|
return PythonBridge::getInstance().callMethodSync(objectId, methodName, options);
|
|
523
|
-
}, options.timeoutMs, options.requestId);
|
|
863
|
+
}, options.timeoutMs, options.requestId, options.callbackDepth);
|
|
524
864
|
}
|
|
525
865
|
|
|
526
866
|
/**
|
|
@@ -890,6 +1230,8 @@ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
890
1230
|
exports.Set("initializePython", Napi::Function::New(env, InitializePython));
|
|
891
1231
|
exports.Set("finalizePython", Napi::Function::New(env, FinalizePython));
|
|
892
1232
|
exports.Set("isInitialized", Napi::Function::New(env, IsInitialized));
|
|
1233
|
+
exports.Set("registerCallback", Napi::Function::New(env, RegisterCallback));
|
|
1234
|
+
exports.Set("disposeCallback", Napi::Function::New(env, DisposeCallback));
|
|
893
1235
|
|
|
894
1236
|
// Module import
|
|
895
1237
|
exports.Set("importModule", Napi::Function::New(env, ImportModule));
|
|
@@ -6,6 +6,7 @@
|
|
|
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>
|
|
@@ -688,6 +689,9 @@ PyObject* PythonBridge::_deserialize(const SerializedValue& value) {
|
|
|
688
689
|
case SerializedFormat::NONE:
|
|
689
690
|
Py_RETURN_NONE;
|
|
690
691
|
|
|
692
|
+
case SerializedFormat::JS_CALLBACK:
|
|
693
|
+
return createPythonCallback(value.metadata.callbackId);
|
|
694
|
+
|
|
691
695
|
default:
|
|
692
696
|
// Try JSON fallback
|
|
693
697
|
if (!value.jsonData.empty()) {
|
|
@@ -697,6 +701,14 @@ PyObject* PythonBridge::_deserialize(const SerializedValue& value) {
|
|
|
697
701
|
}
|
|
698
702
|
}
|
|
699
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
|
+
|
|
700
712
|
// Code Execution
|
|
701
713
|
|
|
702
714
|
TaskResult PythonBridge::runCodeSync(const std::string& code) {
|
|
@@ -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
|
|
|
@@ -214,6 +215,12 @@ public:
|
|
|
214
215
|
void collectGarbage();
|
|
215
216
|
size_t getTrackedObjectCount() const;
|
|
216
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
|
+
|
|
217
224
|
private:
|
|
218
225
|
PythonBridge() = default;
|
|
219
226
|
~PythonBridge() = default;
|
|
@@ -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
|
|
|
@@ -34,6 +34,7 @@ enum class SerializedFormat : uint8_t {
|
|
|
34
34
|
PYTHON_REF = 6,
|
|
35
35
|
BYTES = 7,
|
|
36
36
|
NONE = 8,
|
|
37
|
+
JS_CALLBACK = 9,
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
/**
|
|
@@ -52,6 +53,7 @@ struct SerializedMetadata {
|
|
|
52
53
|
|
|
53
54
|
// For PYTHON_REF
|
|
54
55
|
uint32_t objectId = 0;
|
|
56
|
+
uint32_t callbackId = 0;
|
|
55
57
|
|
|
56
58
|
// General
|
|
57
59
|
int64_t length = 0;
|