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
|
@@ -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
|
/**
|
|
@@ -141,6 +411,12 @@ static Napi::Value InitializePython(const Napi::CallbackInfo& info) {
|
|
|
141
411
|
std::string pythonExecutable = opts.Has("pythonExecutable") ?
|
|
142
412
|
opts.Get("pythonExecutable").As<Napi::String>().Utf8Value() : "python3";
|
|
143
413
|
|
|
414
|
+
std::string pythonBaseExecutable = opts.Has("pythonBaseExecutable") ?
|
|
415
|
+
opts.Get("pythonBaseExecutable").As<Napi::String>().Utf8Value() : pythonExecutable;
|
|
416
|
+
|
|
417
|
+
std::string pythonPrefix = opts.Has("pythonPrefix") ?
|
|
418
|
+
opts.Get("pythonPrefix").As<Napi::String>().Utf8Value() : "";
|
|
419
|
+
|
|
144
420
|
std::string pythonLibrary = opts.Has("pythonLibrary") ?
|
|
145
421
|
opts.Get("pythonLibrary").As<Napi::String>().Utf8Value() : "";
|
|
146
422
|
|
|
@@ -150,6 +426,9 @@ static Napi::Value InitializePython(const Napi::CallbackInfo& info) {
|
|
|
150
426
|
uint32_t maxQueueSize = opts.Has("maxQueueSize") ?
|
|
151
427
|
opts.Get("maxQueueSize").As<Napi::Number>().Uint32Value() : 1000;
|
|
152
428
|
|
|
429
|
+
uint32_t callbackMaxDepth = opts.Has("callbackMaxDepth") ?
|
|
430
|
+
opts.Get("callbackMaxDepth").As<Napi::Number>().Uint32Value() : 0;
|
|
431
|
+
|
|
153
432
|
g_includeTracebacks = opts.Has("includeTracebacks") &&
|
|
154
433
|
opts.Get("includeTracebacks").ToBoolean().Value();
|
|
155
434
|
|
|
@@ -180,10 +459,13 @@ static Napi::Value InitializePython(const Napi::CallbackInfo& info) {
|
|
|
180
459
|
PythonBridge::getInstance().initializePython(
|
|
181
460
|
pythonHome,
|
|
182
461
|
pythonExecutable,
|
|
462
|
+
pythonBaseExecutable,
|
|
463
|
+
pythonPrefix,
|
|
183
464
|
pythonLibrary,
|
|
184
465
|
pythonPathExtra,
|
|
185
466
|
envVars
|
|
186
467
|
);
|
|
468
|
+
g_callbackMaxDepth = callbackMaxDepth > 0 ? callbackMaxDepth : pythonRecursionLimit();
|
|
187
469
|
|
|
188
470
|
// Initialize thread pool
|
|
189
471
|
g_threadPool = std::make_unique<PythonThreadPool>(threadPoolSize, maxQueueSize);
|
|
@@ -217,6 +499,16 @@ static Napi::Value FinalizePython(const Napi::CallbackInfo& info) {
|
|
|
217
499
|
"Cannot finalize Python while calls are active or queued"
|
|
218
500
|
);
|
|
219
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();
|
|
220
512
|
if (g_threadPool) {
|
|
221
513
|
g_threadPool->shutdown();
|
|
222
514
|
g_threadPool.reset();
|
|
@@ -239,6 +531,51 @@ static Napi::Value IsInitialized(const Napi::CallbackInfo& info) {
|
|
|
239
531
|
return Napi::Boolean::New(info.Env(), g_initialized);
|
|
240
532
|
}
|
|
241
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
|
+
|
|
242
579
|
// Generic async wrapper
|
|
243
580
|
|
|
244
581
|
/**
|
|
@@ -249,7 +586,8 @@ static Napi::Value SubmitTask(
|
|
|
249
586
|
Napi::Env env,
|
|
250
587
|
std::function<TaskResult()> work,
|
|
251
588
|
uint32_t timeoutMs = 0,
|
|
252
|
-
std::string cancelKey = {}
|
|
589
|
+
std::string cancelKey = {},
|
|
590
|
+
uint32_t callbackDepth = 0
|
|
253
591
|
) {
|
|
254
592
|
auto deferred = std::make_shared<Napi::Promise::Deferred>(
|
|
255
593
|
Napi::Promise::Deferred::New(env)
|
|
@@ -261,8 +599,12 @@ static Napi::Value SubmitTask(
|
|
|
261
599
|
}
|
|
262
600
|
|
|
263
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
|
+
};
|
|
264
606
|
g_threadPool->submit(
|
|
265
|
-
std::move(
|
|
607
|
+
std::move(scopedWork),
|
|
266
608
|
[deferred](TaskResult result) {
|
|
267
609
|
Napi::Env callEnv = deferred->Env();
|
|
268
610
|
Napi::HandleScope scope(callEnv);
|
|
@@ -473,11 +815,14 @@ static Napi::Value CallFunction(const Napi::CallbackInfo& info) {
|
|
|
473
815
|
if (optsObj.Has("requestId") && optsObj.Get("requestId").IsString()) {
|
|
474
816
|
options.requestId = optsObj.Get("requestId").As<Napi::String>().Utf8Value();
|
|
475
817
|
}
|
|
818
|
+
if (optsObj.Has("callbackDepth") && optsObj.Get("callbackDepth").IsNumber()) {
|
|
819
|
+
options.callbackDepth = optsObj.Get("callbackDepth").As<Napi::Number>().Uint32Value();
|
|
820
|
+
}
|
|
476
821
|
}
|
|
477
822
|
|
|
478
823
|
return SubmitTask(env, [objectId, path, options]() -> TaskResult {
|
|
479
824
|
return PythonBridge::getInstance().callFunctionSync(objectId, path, options);
|
|
480
|
-
}, options.timeoutMs, options.requestId);
|
|
825
|
+
}, options.timeoutMs, options.requestId, options.callbackDepth);
|
|
481
826
|
}
|
|
482
827
|
|
|
483
828
|
/**
|
|
@@ -508,11 +853,14 @@ static Napi::Value CallMethod(const Napi::CallbackInfo& info) {
|
|
|
508
853
|
if (optsObj.Has("requestId") && optsObj.Get("requestId").IsString()) {
|
|
509
854
|
options.requestId = optsObj.Get("requestId").As<Napi::String>().Utf8Value();
|
|
510
855
|
}
|
|
856
|
+
if (optsObj.Has("callbackDepth") && optsObj.Get("callbackDepth").IsNumber()) {
|
|
857
|
+
options.callbackDepth = optsObj.Get("callbackDepth").As<Napi::Number>().Uint32Value();
|
|
858
|
+
}
|
|
511
859
|
}
|
|
512
860
|
|
|
513
861
|
return SubmitTask(env, [objectId, methodName, options]() -> TaskResult {
|
|
514
862
|
return PythonBridge::getInstance().callMethodSync(objectId, methodName, options);
|
|
515
|
-
}, options.timeoutMs, options.requestId);
|
|
863
|
+
}, options.timeoutMs, options.requestId, options.callbackDepth);
|
|
516
864
|
}
|
|
517
865
|
|
|
518
866
|
/**
|
|
@@ -882,6 +1230,8 @@ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
882
1230
|
exports.Set("initializePython", Napi::Function::New(env, InitializePython));
|
|
883
1231
|
exports.Set("finalizePython", Napi::Function::New(env, FinalizePython));
|
|
884
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));
|
|
885
1235
|
|
|
886
1236
|
// Module import
|
|
887
1237
|
exports.Set("importModule", Napi::Function::New(env, ImportModule));
|