pynect 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +478 -2
  3. package/SECURITY.md +34 -0
  4. package/binding.gyp +60 -0
  5. package/dist/cli/codegen.d.ts +3 -0
  6. package/dist/cli/codegen.d.ts.map +1 -0
  7. package/dist/cli/codegen.js +543 -0
  8. package/dist/cli/codegen.js.map +1 -0
  9. package/dist/cli/sync.d.ts +3 -0
  10. package/dist/cli/sync.d.ts.map +1 -0
  11. package/dist/cli/sync.js +48 -0
  12. package/dist/cli/sync.js.map +1 -0
  13. package/dist/config/project.d.ts +57 -0
  14. package/dist/config/project.d.ts.map +1 -0
  15. package/dist/config/project.js +176 -0
  16. package/dist/config/project.js.map +1 -0
  17. package/dist/core/IsolatedWorker.d.ts +39 -0
  18. package/dist/core/IsolatedWorker.d.ts.map +1 -0
  19. package/dist/core/IsolatedWorker.js +178 -0
  20. package/dist/core/IsolatedWorker.js.map +1 -0
  21. package/dist/core/PyCallable.d.ts +67 -0
  22. package/dist/core/PyCallable.d.ts.map +1 -0
  23. package/dist/core/PyCallable.js +104 -0
  24. package/dist/core/PyCallable.js.map +1 -0
  25. package/dist/core/PyContext.d.ts +76 -0
  26. package/dist/core/PyContext.d.ts.map +1 -0
  27. package/dist/core/PyContext.js +239 -0
  28. package/dist/core/PyContext.js.map +1 -0
  29. package/dist/core/PyIterator.d.ts +84 -0
  30. package/dist/core/PyIterator.d.ts.map +1 -0
  31. package/dist/core/PyIterator.js +243 -0
  32. package/dist/core/PyIterator.js.map +1 -0
  33. package/dist/core/PyModule.d.ts +53 -0
  34. package/dist/core/PyModule.d.ts.map +1 -0
  35. package/dist/core/PyModule.js +162 -0
  36. package/dist/core/PyModule.js.map +1 -0
  37. package/dist/core/PyProxy.d.ts +75 -0
  38. package/dist/core/PyProxy.d.ts.map +1 -0
  39. package/dist/core/PyProxy.js +563 -0
  40. package/dist/core/PyProxy.js.map +1 -0
  41. package/dist/core/PyRuntime.d.ts +112 -0
  42. package/dist/core/PyRuntime.d.ts.map +1 -0
  43. package/dist/core/PyRuntime.js +546 -0
  44. package/dist/core/PyRuntime.js.map +1 -0
  45. package/dist/env/PackageInstaller.d.ts +232 -0
  46. package/dist/env/PackageInstaller.d.ts.map +1 -0
  47. package/dist/env/PackageInstaller.js +628 -0
  48. package/dist/env/PackageInstaller.js.map +1 -0
  49. package/dist/env/ProjectEnvironment.d.ts +63 -0
  50. package/dist/env/ProjectEnvironment.d.ts.map +1 -0
  51. package/dist/env/ProjectEnvironment.js +379 -0
  52. package/dist/env/ProjectEnvironment.js.map +1 -0
  53. package/dist/env/PyfigAdapter.d.ts +32 -0
  54. package/dist/env/PyfigAdapter.d.ts.map +1 -0
  55. package/dist/env/PyfigAdapter.js +10 -0
  56. package/dist/env/PyfigAdapter.js.map +1 -0
  57. package/dist/env/PythonDetector.d.ts +48 -0
  58. package/dist/env/PythonDetector.d.ts.map +1 -0
  59. package/dist/env/PythonDetector.js +182 -0
  60. package/dist/env/PythonDetector.js.map +1 -0
  61. package/dist/env/PythonProvisioner.d.ts +16 -0
  62. package/dist/env/PythonProvisioner.d.ts.map +1 -0
  63. package/dist/env/PythonProvisioner.js +42 -0
  64. package/dist/env/PythonProvisioner.js.map +1 -0
  65. package/dist/env/VenvManager.d.ts +55 -0
  66. package/dist/env/VenvManager.d.ts.map +1 -0
  67. package/dist/env/VenvManager.js +153 -0
  68. package/dist/env/VenvManager.js.map +1 -0
  69. package/dist/env/executables.d.ts +3 -0
  70. package/dist/env/executables.d.ts.map +1 -0
  71. package/dist/env/executables.js +14 -0
  72. package/dist/env/executables.js.map +1 -0
  73. package/dist/index.d.ts +182 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/index.js +393 -0
  76. package/dist/index.js.map +1 -0
  77. package/dist/native/NativeAddonLoader.d.ts +11 -0
  78. package/dist/native/NativeAddonLoader.d.ts.map +1 -0
  79. package/dist/native/NativeAddonLoader.js +75 -0
  80. package/dist/native/NativeAddonLoader.js.map +1 -0
  81. package/dist/plugins/Plugin.interface.d.ts +41 -0
  82. package/dist/plugins/Plugin.interface.d.ts.map +1 -0
  83. package/dist/plugins/Plugin.interface.js +12 -0
  84. package/dist/plugins/Plugin.interface.js.map +1 -0
  85. package/dist/plugins/PluginManager.d.ts +26 -0
  86. package/dist/plugins/PluginManager.d.ts.map +1 -0
  87. package/dist/plugins/PluginManager.js +174 -0
  88. package/dist/plugins/PluginManager.js.map +1 -0
  89. package/dist/plugins/builtin/NumpyPlugin.d.ts +17 -0
  90. package/dist/plugins/builtin/NumpyPlugin.d.ts.map +1 -0
  91. package/dist/plugins/builtin/NumpyPlugin.js +41 -0
  92. package/dist/plugins/builtin/NumpyPlugin.js.map +1 -0
  93. package/dist/plugins/builtin/PandasPlugin.d.ts +19 -0
  94. package/dist/plugins/builtin/PandasPlugin.d.ts.map +1 -0
  95. package/dist/plugins/builtin/PandasPlugin.js +57 -0
  96. package/dist/plugins/builtin/PandasPlugin.js.map +1 -0
  97. package/dist/plugins/builtin/TorchPlugin.d.ts +23 -0
  98. package/dist/plugins/builtin/TorchPlugin.d.ts.map +1 -0
  99. package/dist/plugins/builtin/TorchPlugin.js +50 -0
  100. package/dist/plugins/builtin/TorchPlugin.js.map +1 -0
  101. package/dist/plugins/index.d.ts +7 -0
  102. package/dist/plugins/index.d.ts.map +1 -0
  103. package/dist/plugins/index.js +12 -0
  104. package/dist/plugins/index.js.map +1 -0
  105. package/dist/serialization/DataFrameBridge.d.ts +141 -0
  106. package/dist/serialization/DataFrameBridge.d.ts.map +1 -0
  107. package/dist/serialization/DataFrameBridge.js +355 -0
  108. package/dist/serialization/DataFrameBridge.js.map +1 -0
  109. package/dist/serialization/MsgPackSerializer.d.ts +45 -0
  110. package/dist/serialization/MsgPackSerializer.d.ts.map +1 -0
  111. package/dist/serialization/MsgPackSerializer.js +242 -0
  112. package/dist/serialization/MsgPackSerializer.js.map +1 -0
  113. package/dist/serialization/NumpyBridge.d.ts +96 -0
  114. package/dist/serialization/NumpyBridge.d.ts.map +1 -0
  115. package/dist/serialization/NumpyBridge.js +323 -0
  116. package/dist/serialization/NumpyBridge.js.map +1 -0
  117. package/dist/serialization/Serializer.d.ts +78 -0
  118. package/dist/serialization/Serializer.d.ts.map +1 -0
  119. package/dist/serialization/Serializer.js +281 -0
  120. package/dist/serialization/Serializer.js.map +1 -0
  121. package/dist/types/addon.d.ts +131 -0
  122. package/dist/types/addon.d.ts.map +1 -0
  123. package/dist/types/addon.js +32 -0
  124. package/dist/types/addon.js.map +1 -0
  125. package/dist/types/config.d.ts +233 -0
  126. package/dist/types/config.d.ts.map +1 -0
  127. package/dist/types/config.js +60 -0
  128. package/dist/types/config.js.map +1 -0
  129. package/dist/types/index.d.ts +10 -0
  130. package/dist/types/index.d.ts.map +1 -0
  131. package/dist/types/index.js +12 -0
  132. package/dist/types/index.js.map +1 -0
  133. package/dist/types/python.d.ts +235 -0
  134. package/dist/types/python.d.ts.map +1 -0
  135. package/dist/types/python.js +7 -0
  136. package/dist/types/python.js.map +1 -0
  137. package/dist/utils/ErrorTranslator.d.ts +83 -0
  138. package/dist/utils/ErrorTranslator.d.ts.map +1 -0
  139. package/dist/utils/ErrorTranslator.js +210 -0
  140. package/dist/utils/ErrorTranslator.js.map +1 -0
  141. package/dist/utils/Locking.d.ts +3 -0
  142. package/dist/utils/Locking.d.ts.map +1 -0
  143. package/dist/utils/Locking.js +10 -0
  144. package/dist/utils/Locking.js.map +1 -0
  145. package/dist/utils/Logger.d.ts +27 -0
  146. package/dist/utils/Logger.d.ts.map +1 -0
  147. package/dist/utils/Logger.js +115 -0
  148. package/dist/utils/Logger.js.map +1 -0
  149. package/dist/utils/MemoryMonitor.d.ts +43 -0
  150. package/dist/utils/MemoryMonitor.d.ts.map +1 -0
  151. package/dist/utils/MemoryMonitor.js +143 -0
  152. package/dist/utils/MemoryMonitor.js.map +1 -0
  153. package/package.json +114 -4
  154. package/python/ast_inspector.py +248 -0
  155. package/python/error_handler.py +432 -0
  156. package/python/isolated_worker.py +94 -0
  157. package/python/pynect_dispatcher.py +382 -0
  158. package/python/pynect_runtime.py +574 -0
  159. package/python/serializer.py +373 -0
  160. package/python/type_inspector.py +315 -0
  161. package/scripts/build-native.js +50 -0
  162. package/scripts/cli.js +21 -0
  163. package/scripts/ensure-native.js +85 -0
  164. package/scripts/postinstall.js +40 -0
  165. package/scripts/pyfig.js +113 -0
  166. package/scripts/python-provisioner.js +23 -0
  167. package/scripts/smoke-python-boundary.js +103 -0
  168. package/src/native/NativeAddonLoader.ts +98 -0
  169. package/src/native/gil_guard.cpp +25 -0
  170. package/src/native/gil_guard.h +174 -0
  171. package/src/native/pynect_addon.cpp +942 -0
  172. package/src/native/python_bridge.cpp +904 -0
  173. package/src/native/python_bridge.h +263 -0
  174. package/src/native/thread_pool.cpp +375 -0
  175. package/src/native/thread_pool.h +185 -0
  176. package/src/native/type_converter.cpp +906 -0
  177. package/src/native/type_converter.h +271 -0
@@ -0,0 +1,904 @@
1
+ /**
2
+ * pynect - Python Bridge Implementation
3
+ * Core Python operations: import, eval, attribute access, function calls.
4
+ */
5
+
6
+ #include "python_bridge.h"
7
+ #include "gil_guard.h"
8
+ #include "type_converter.h"
9
+
10
+ #include <Python.h>
11
+ #include <sstream>
12
+ #include <stdexcept>
13
+ #include <iostream>
14
+ #include <algorithm>
15
+
16
+ #ifdef __linux__
17
+ #include <dlfcn.h>
18
+ #endif
19
+
20
+ namespace pynect {
21
+
22
+ namespace {
23
+
24
+ #ifdef __linux__
25
+ void promotePythonLibrary(const std::string& libraryPath) {
26
+ if (libraryPath.empty()) {
27
+ throw std::runtime_error("Python library path is required on Linux");
28
+ }
29
+
30
+ // Node loads addons with local visibility. CPython extension modules are
31
+ // normally linked against symbols supplied by the embedding process, so
32
+ // promote the exact Pyfig-selected libpython before importing any of them.
33
+ void* handle = dlopen(libraryPath.c_str(), RTLD_NOW | RTLD_GLOBAL);
34
+ if (!handle) {
35
+ const char* detail = dlerror();
36
+ throw std::runtime_error(
37
+ "Failed to expose Python library symbols: " +
38
+ std::string(detail ? detail : "unknown dynamic loader error")
39
+ );
40
+ }
41
+
42
+ // CPython owns process-wide state, and unloading libpython before process
43
+ // exit is unsafe. Retain every promoted handle for the process lifetime.
44
+ static std::vector<void*> pythonLibraryHandles;
45
+ pythonLibraryHandles.push_back(handle);
46
+ }
47
+ #endif
48
+
49
+ } // namespace
50
+
51
+ // Singleton
52
+
53
+ PythonBridge& PythonBridge::getInstance() {
54
+ static PythonBridge instance;
55
+ return instance;
56
+ }
57
+
58
+ // Initialize Python
59
+
60
+ void PythonBridge::initializePython(
61
+ const std::string& pythonHome,
62
+ const std::string& pythonExecutable,
63
+ const std::string& pythonLibrary,
64
+ const std::vector<std::string>& extraSysPaths,
65
+ const std::vector<std::pair<std::string, std::string>>& envVars
66
+ ) {
67
+ if (_initialized) {
68
+ throw std::runtime_error("Python already initialized");
69
+ }
70
+
71
+ #ifdef __linux__
72
+ promotePythonLibrary(pythonLibrary);
73
+ #endif
74
+
75
+ // Set environment variables before initialization
76
+ for (const auto& [key, val] : envVars) {
77
+ #ifdef _WIN32
78
+ _putenv_s(key.c_str(), val.c_str());
79
+ #else
80
+ setenv(key.c_str(), val.c_str(), 1);
81
+ #endif
82
+ }
83
+
84
+ // Configure the interpreter with the detected executable so CPython can
85
+ // resolve its standard library consistently across platforms.
86
+ PyConfig config;
87
+ PyConfig_InitPythonConfig(&config);
88
+ PyStatus status = PyConfig_SetBytesString(
89
+ &config,
90
+ &config.program_name,
91
+ pythonExecutable.c_str()
92
+ );
93
+ if (!PyStatus_Exception(status) && !pythonHome.empty()) {
94
+ status = PyConfig_SetBytesString(&config, &config.home, pythonHome.c_str());
95
+ }
96
+ if (!PyStatus_Exception(status)) {
97
+ status = Py_InitializeFromConfig(&config);
98
+ }
99
+ std::string configError = PyStatus_Exception(status) && status.err_msg
100
+ ? status.err_msg
101
+ : "";
102
+ PyConfig_Clear(&config);
103
+ if (PyStatus_Exception(status)) {
104
+ throw std::runtime_error("Failed to configure Python: " + configError);
105
+ }
106
+
107
+ if (!Py_IsInitialized()) {
108
+ throw std::runtime_error("Failed to initialize Python interpreter");
109
+ }
110
+
111
+ // Enable thread support
112
+ PyEval_InitThreads();
113
+
114
+ // Save main thread state and release GIL so threads can run
115
+ _mainThreadState = PyEval_SaveThread();
116
+
117
+ // Add extra sys.path entries (done with GIL held)
118
+ if (!extraSysPaths.empty()) {
119
+ GILAcquire gil;
120
+ PyObject* sysModule = PyImport_ImportModule("sys");
121
+ if (sysModule) {
122
+ PyRef sysRef = PyRef::steal(sysModule);
123
+ PyObject* sysPath = PyObject_GetAttrString(sysModule, "path");
124
+ if (sysPath) {
125
+ PyRef pathRef = PyRef::steal(sysPath);
126
+ for (const auto& p : extraSysPaths) {
127
+ PyObject* pathStr = PyUnicode_FromString(p.c_str());
128
+ if (pathStr) {
129
+ PyList_Insert(sysPath, 0, pathStr);
130
+ Py_DECREF(pathStr);
131
+ }
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ _initialized = true;
138
+ }
139
+
140
+ // Finalize Python
141
+
142
+ void PythonBridge::finalizePython() {
143
+ if (!_initialized) return;
144
+
145
+ // Re-acquire main thread's GIL before calling Py_Finalize
146
+ if (_mainThreadState) {
147
+ PyEval_RestoreThread(_mainThreadState);
148
+ _mainThreadState = nullptr;
149
+ }
150
+
151
+ // Clear iterators
152
+ {
153
+ std::lock_guard<std::mutex> lock(_iteratorsMutex);
154
+ for (auto& [id, iter] : _iterators) {
155
+ Py_XDECREF(iter);
156
+ }
157
+ _iterators.clear();
158
+ }
159
+
160
+ // Clear object registry
161
+ TypeConverter::clearRegistry();
162
+
163
+ // Finalize Python
164
+ Py_Finalize();
165
+ _initialized = false;
166
+ }
167
+
168
+ // Helper: Make Error Result
169
+
170
+ TaskResult PythonBridge::_makeErrorResult() {
171
+ TaskResult result;
172
+ result.success = false;
173
+ auto [type, msg, tb] = TypeConverter::captureException();
174
+ result.errorType = type;
175
+ result.errorMessage = msg;
176
+ result.errorTraceback = tb;
177
+ return result;
178
+ }
179
+
180
+ // Helper: Make Success Result
181
+
182
+ TaskResult PythonBridge::_makeSuccessResult(PyObject* obj) {
183
+ TaskResult result;
184
+ result.success = true;
185
+
186
+ if (!obj || obj == Py_None) {
187
+ result.resultJson = "null";
188
+ return result;
189
+ }
190
+
191
+ // For objects that can't be fully serialized, register as opaque ref
192
+ if (TypeConverter::isNumpyArray(obj) ||
193
+ TypeConverter::isDataFrame(obj) ||
194
+ TypeConverter::isSeries(obj) ||
195
+ TypeConverter::isTorchTensor(obj) ||
196
+ TypeConverter::isModule(obj) ||
197
+ TypeConverter::isClass(obj)) {
198
+ // Always return as object reference for these types
199
+ result.objectId = TypeConverter::registerObject(obj);
200
+ result.isObject = true;
201
+ result.resultJson = "{\"__pyref__\":" + std::to_string(result.objectId) + "}";
202
+ return result;
203
+ }
204
+
205
+ // For callable objects
206
+ if (PyCallable_Check(obj) && !PyFunction_Check(obj) &&
207
+ !PyModule_Check(obj) && !PyType_Check(obj)) {
208
+ // Instance of a class with __call__ - usually want as reference
209
+ result.objectId = TypeConverter::registerObject(obj);
210
+ result.isObject = true;
211
+ result.resultJson = "{\"__pyref__\":" + std::to_string(result.objectId) + "}";
212
+ return result;
213
+ }
214
+
215
+ SerializedValue sv = TypeConverter::pythonToSerialized(obj);
216
+
217
+ if (sv.format == SerializedFormat::PYTHON_REF) {
218
+ result.objectId = sv.metadata.objectId;
219
+ result.isObject = true;
220
+ result.resultJson = "{\"__pyref__\":" + std::to_string(result.objectId) + "}";
221
+ } else if (sv.format == SerializedFormat::JSON) {
222
+ result.resultJson = sv.jsonData;
223
+ result.isObject = false;
224
+ } else {
225
+ // Binary format - register as object for now
226
+ // In production, pass binary data through
227
+ result.objectId = TypeConverter::registerObject(obj);
228
+ result.isObject = true;
229
+ result.resultJson = "{\"__pyref__\":" + std::to_string(result.objectId) + "}";
230
+ }
231
+
232
+ return result;
233
+ }
234
+
235
+ // Module Import
236
+
237
+ TaskResult PythonBridge::importModuleSync(const std::string& moduleName) {
238
+ // GIL must be held by caller
239
+ PyObject* dispatcher = PyImport_ImportModule("pynect_dispatcher");
240
+ if (!dispatcher) {
241
+ return _makeErrorResult();
242
+ }
243
+ PyRef dispatcherRef = PyRef::steal(dispatcher);
244
+ PyObject* isAllowed = PyObject_GetAttrString(dispatcher, "is_module_allowed");
245
+ if (!isAllowed) {
246
+ return _makeErrorResult();
247
+ }
248
+ PyRef isAllowedRef = PyRef::steal(isAllowed);
249
+ PyObject* allowed = PyObject_CallFunction(isAllowed, "s", moduleName.c_str());
250
+ if (!allowed) {
251
+ return _makeErrorResult();
252
+ }
253
+ PyRef allowedRef = PyRef::steal(allowed);
254
+ if (PyObject_IsTrue(allowed) != 1) {
255
+ PyErr_Format(PyExc_PermissionError, "Python module is not allowed: %s", moduleName.c_str());
256
+ return _makeErrorResult();
257
+ }
258
+
259
+ PyObject* module = PyImport_ImportModule(moduleName.c_str());
260
+ if (!module) {
261
+ return _makeErrorResult();
262
+ }
263
+
264
+ PyRef modRef = PyRef::steal(module);
265
+ TaskResult result;
266
+ result.success = true;
267
+ result.objectId = TypeConverter::registerObject(module);
268
+ result.isObject = true;
269
+ result.resultJson = "{\"__pyref__\":" + std::to_string(result.objectId) + "}";
270
+ return result;
271
+ }
272
+
273
+ TaskResult PythonBridge::reloadModuleSync(uint32_t objectId) {
274
+ PyObject* module = TypeConverter::getObject(objectId);
275
+ if (!module) {
276
+ TaskResult r;
277
+ r.success = false;
278
+ r.errorType = "ValueError";
279
+ r.errorMessage = "Object ID " + std::to_string(objectId) + " not found";
280
+ return r;
281
+ }
282
+
283
+ PyObject* reloaded = PyImport_ReloadModule(module);
284
+ if (!reloaded) {
285
+ return _makeErrorResult();
286
+ }
287
+
288
+ PyRef reloadedRef = PyRef::steal(reloaded);
289
+
290
+ // Update registry
291
+ TypeConverter::decRef(objectId);
292
+ uint32_t newId = TypeConverter::registerObject(reloaded);
293
+
294
+ TaskResult result;
295
+ result.success = true;
296
+ result.objectId = newId;
297
+ result.isObject = true;
298
+ result.resultJson = "{\"__pyref__\":" + std::to_string(newId) + "}";
299
+ return result;
300
+ }
301
+
302
+ // Attribute Access
303
+
304
+ TaskResult PythonBridge::getAttributeSync(uint32_t objectId, const std::string& attrName) {
305
+ PyObject* obj = TypeConverter::getObject(objectId);
306
+ if (!obj) {
307
+ TaskResult r;
308
+ r.success = false;
309
+ r.errorType = "ValueError";
310
+ r.errorMessage = "Object not found: id=" + std::to_string(objectId);
311
+ return r;
312
+ }
313
+
314
+ PyObject* attr = PyObject_GetAttrString(obj, attrName.c_str());
315
+ if (!attr) {
316
+ return _makeErrorResult();
317
+ }
318
+
319
+ PyRef attrRef = PyRef::steal(attr);
320
+ return _makeSuccessResult(attr);
321
+ }
322
+
323
+ TaskResult PythonBridge::getAttributePathSync(
324
+ uint32_t objectId,
325
+ const std::vector<std::string>& path
326
+ ) {
327
+ PyObject* obj = TypeConverter::getObject(objectId);
328
+ if (!obj) {
329
+ TaskResult r;
330
+ r.success = false;
331
+ r.errorType = "ValueError";
332
+ r.errorMessage = "Object not found: id=" + std::to_string(objectId);
333
+ return r;
334
+ }
335
+
336
+ if (path.empty()) {
337
+ return _makeSuccessResult(obj);
338
+ }
339
+
340
+ PyObject* resolved = _resolveAttributePath(obj, path);
341
+ if (!resolved) {
342
+ return _makeErrorResult();
343
+ }
344
+
345
+ PyRef resolvedRef = PyRef::steal(resolved);
346
+ return _makeSuccessResult(resolved);
347
+ }
348
+
349
+ PyObject* PythonBridge::_resolveAttributePath(
350
+ PyObject* obj,
351
+ const std::vector<std::string>& path
352
+ ) {
353
+ if (!obj || path.empty()) {
354
+ if (obj) Py_INCREF(obj);
355
+ return obj;
356
+ }
357
+
358
+ Py_INCREF(obj);
359
+ PyObject* current = obj;
360
+
361
+ for (const auto& attr : path) {
362
+ PyObject* next = PyObject_GetAttrString(current, attr.c_str());
363
+ Py_DECREF(current);
364
+
365
+ if (!next) {
366
+ return nullptr; // Exception set
367
+ }
368
+ current = next;
369
+ }
370
+
371
+ return current; // new reference
372
+ }
373
+
374
+ TaskResult PythonBridge::setAttributeSync(
375
+ uint32_t objectId,
376
+ const std::string& attrName,
377
+ const SerializedValue& value
378
+ ) {
379
+ PyObject* obj = TypeConverter::getObject(objectId);
380
+ if (!obj) {
381
+ TaskResult r;
382
+ r.success = false;
383
+ r.errorType = "ValueError";
384
+ r.errorMessage = "Object not found";
385
+ return r;
386
+ }
387
+
388
+ PyObject* pyValue = _deserialize(value);
389
+ if (!pyValue) {
390
+ TaskResult r;
391
+ r.success = false;
392
+ r.errorType = "ValueError";
393
+ r.errorMessage = "Failed to deserialize value";
394
+ return r;
395
+ }
396
+ PyRef valueRef = PyRef::steal(pyValue);
397
+
398
+ int ret = PyObject_SetAttrString(obj, attrName.c_str(), pyValue);
399
+ if (ret != 0) {
400
+ return _makeErrorResult();
401
+ }
402
+
403
+ TaskResult result;
404
+ result.success = true;
405
+ result.resultJson = "true";
406
+ return result;
407
+ }
408
+
409
+ bool PythonBridge::hasAttributeSync(uint32_t objectId, const std::string& attrName) {
410
+ PyObject* obj = TypeConverter::getObject(objectId);
411
+ if (!obj) return false;
412
+ return PyObject_HasAttrString(obj, attrName.c_str()) == 1;
413
+ }
414
+
415
+ TaskResult PythonBridge::deleteAttributeSync(uint32_t objectId, const std::string& attrName) {
416
+ PyObject* obj = TypeConverter::getObject(objectId);
417
+ if (!obj) {
418
+ TaskResult r;
419
+ r.success = false;
420
+ r.errorType = "ValueError";
421
+ r.errorMessage = "Object not found";
422
+ return r;
423
+ }
424
+
425
+ int ret = PyObject_DelAttrString(obj, attrName.c_str());
426
+ if (ret != 0) {
427
+ return _makeErrorResult();
428
+ }
429
+
430
+ TaskResult result;
431
+ result.success = true;
432
+ result.resultJson = "true";
433
+ return result;
434
+ }
435
+
436
+ // Function Calls
437
+
438
+ TaskResult PythonBridge::callFunctionSync(
439
+ uint32_t objectId,
440
+ const std::vector<std::string>& path,
441
+ const CallOptions& options
442
+ ) {
443
+ PyObject* obj = TypeConverter::getObject(objectId);
444
+ if (!obj) {
445
+ TaskResult r;
446
+ r.success = false;
447
+ r.errorType = "ValueError";
448
+ r.errorMessage = "Object not found: id=" + std::to_string(objectId);
449
+ return r;
450
+ }
451
+
452
+ // Resolve path to callable
453
+ PyObject* callable;
454
+ if (path.empty()) {
455
+ Py_INCREF(obj);
456
+ callable = obj;
457
+ } else {
458
+ callable = _resolveAttributePath(obj, path);
459
+ if (!callable) {
460
+ return _makeErrorResult();
461
+ }
462
+ }
463
+ PyRef callableRef = PyRef::steal(callable);
464
+
465
+ if (!PyCallable_Check(callable)) {
466
+ // The result is the object at the path, not a callable
467
+ // Return it as a value
468
+ return _makeSuccessResult(callable);
469
+ }
470
+
471
+ // Build args and kwargs
472
+ PyObject* argsObj = nullptr;
473
+ PyObject* kwargsObj = nullptr;
474
+
475
+ if (!_buildCallArgs(options, argsObj, kwargsObj)) {
476
+ Py_XDECREF(argsObj);
477
+ Py_XDECREF(kwargsObj);
478
+ return _makeErrorResult();
479
+ }
480
+
481
+ PyRef argsRef = PyRef::steal(argsObj);
482
+ PyRef kwargsRef = PyRef::steal(kwargsObj);
483
+
484
+ // Route every call through the policy/validation dispatcher. Pydantic is
485
+ // imported lazily there and is not required by the native addon.
486
+ PyObject* dispatcher = PyImport_ImportModule("pynect_dispatcher");
487
+ if (!dispatcher) {
488
+ return _makeErrorResult();
489
+ }
490
+ PyRef dispatcherRef = PyRef::steal(dispatcher);
491
+ PyObject* invoke = PyObject_GetAttrString(dispatcher, "invoke");
492
+ if (!invoke) {
493
+ return _makeErrorResult();
494
+ }
495
+ PyRef invokeRef = PyRef::steal(invoke);
496
+ PyObject* requestId = PyUnicode_FromString(options.requestId.c_str());
497
+ if (!requestId) {
498
+ return _makeErrorResult();
499
+ }
500
+ PyRef requestIdRef = PyRef::steal(requestId);
501
+ PyObject* executionTimeout = PyLong_FromUnsignedLong(options.executionTimeoutMs);
502
+ if (!executionTimeout) {
503
+ return _makeErrorResult();
504
+ }
505
+ PyRef executionTimeoutRef = PyRef::steal(executionTimeout);
506
+ PyObject* result = PyObject_CallFunctionObjArgs(
507
+ invoke,
508
+ callable,
509
+ argsObj,
510
+ kwargsObj,
511
+ requestId,
512
+ executionTimeout,
513
+ nullptr
514
+ );
515
+
516
+ if (!result) {
517
+ return _makeErrorResult();
518
+ }
519
+
520
+ PyRef resultRef = PyRef::steal(result);
521
+ return _makeSuccessResult(result);
522
+ }
523
+
524
+ TaskResult PythonBridge::callMethodSync(
525
+ uint32_t objectId,
526
+ const std::string& methodName,
527
+ const CallOptions& options
528
+ ) {
529
+ return callFunctionSync(objectId, {methodName}, options);
530
+ }
531
+
532
+ TaskResult PythonBridge::cancelCallSync(const std::string& requestId) {
533
+ PyObject* dispatcher = PyImport_ImportModule("pynect_dispatcher");
534
+ if (!dispatcher) return _makeErrorResult();
535
+ PyRef dispatcherRef = PyRef::steal(dispatcher);
536
+ PyObject* cancel = PyObject_GetAttrString(dispatcher, "cancel");
537
+ if (!cancel) return _makeErrorResult();
538
+ PyRef cancelRef = PyRef::steal(cancel);
539
+ PyObject* result = PyObject_CallFunction(cancel, "s", requestId.c_str());
540
+ if (!result) return _makeErrorResult();
541
+ PyRef resultRef = PyRef::steal(result);
542
+ TaskResult output;
543
+ output.success = true;
544
+ output.resultJson = PyObject_IsTrue(result) ? "true" : "false";
545
+ return output;
546
+ }
547
+
548
+ // Build Call Args
549
+
550
+ bool PythonBridge::_buildCallArgs(
551
+ const CallOptions& options,
552
+ PyObject*& argsOut,
553
+ PyObject*& kwargsOut
554
+ ) {
555
+ // Build positional args tuple
556
+ argsOut = PyTuple_New(static_cast<Py_ssize_t>(options.args.size()));
557
+ if (!argsOut) return false;
558
+
559
+ for (size_t i = 0; i < options.args.size(); ++i) {
560
+ PyObject* arg = _deserialize(options.args[i]);
561
+ if (!arg) {
562
+ Py_DECREF(argsOut);
563
+ argsOut = nullptr;
564
+ return false;
565
+ }
566
+ PyTuple_SET_ITEM(argsOut, static_cast<Py_ssize_t>(i), arg); // steals reference
567
+ }
568
+
569
+ // Build kwargs dict
570
+ kwargsOut = PyDict_New();
571
+ if (!kwargsOut) {
572
+ Py_DECREF(argsOut);
573
+ argsOut = nullptr;
574
+ return false;
575
+ }
576
+
577
+ for (const auto& [key, val] : options.kwargs) {
578
+ PyObject* pyVal = _deserialize(val);
579
+ if (!pyVal) {
580
+ Py_DECREF(argsOut);
581
+ Py_DECREF(kwargsOut);
582
+ argsOut = nullptr;
583
+ kwargsOut = nullptr;
584
+ return false;
585
+ }
586
+ PyDict_SetItemString(kwargsOut, key.c_str(), pyVal);
587
+ Py_DECREF(pyVal);
588
+ }
589
+
590
+ return true;
591
+ }
592
+
593
+ // Deserialize
594
+
595
+ PyObject* PythonBridge::_deserialize(const SerializedValue& value) {
596
+ switch (value.format) {
597
+ case SerializedFormat::JSON:
598
+ return TypeConverter::jsonToPython(value.jsonData);
599
+
600
+ case SerializedFormat::MSGPACK:
601
+ return TypeConverter::msgpackToPython(value.binaryData);
602
+
603
+ case SerializedFormat::NUMPY_ARRAY:
604
+ return TypeConverter::bufferToNumpyArray(
605
+ value.binaryData,
606
+ value.metadata.dtype,
607
+ value.metadata.shape
608
+ );
609
+
610
+ case SerializedFormat::PYTHON_REF: {
611
+ PyObject* obj = TypeConverter::getObject(value.metadata.objectId);
612
+ if (!obj) {
613
+ PyErr_SetString(PyExc_ValueError, "Python object reference not found");
614
+ return nullptr;
615
+ }
616
+ Py_INCREF(obj);
617
+ return obj;
618
+ }
619
+
620
+ case SerializedFormat::BYTES:
621
+ return PyBytes_FromStringAndSize(
622
+ reinterpret_cast<const char*>(value.binaryData.data()),
623
+ static_cast<Py_ssize_t>(value.binaryData.size())
624
+ );
625
+
626
+ case SerializedFormat::NONE:
627
+ Py_RETURN_NONE;
628
+
629
+ default:
630
+ // Try JSON fallback
631
+ if (!value.jsonData.empty()) {
632
+ return TypeConverter::jsonToPython(value.jsonData);
633
+ }
634
+ Py_RETURN_NONE;
635
+ }
636
+ }
637
+
638
+ // Code Execution
639
+
640
+ TaskResult PythonBridge::runCodeSync(const std::string& code) {
641
+ PyObject* mainModule = PyImport_AddModule("__main__");
642
+ if (!mainModule) return _makeErrorResult();
643
+
644
+ PyObject* globalDict = PyModule_GetDict(mainModule); // borrowed
645
+
646
+ PyObject* result = PyRun_String(code.c_str(), Py_file_input, globalDict, globalDict);
647
+ if (!result) {
648
+ return _makeErrorResult();
649
+ }
650
+
651
+ PyRef resultRef = PyRef::steal(result);
652
+ TaskResult r;
653
+ r.success = true;
654
+ r.resultJson = "null";
655
+ return r;
656
+ }
657
+
658
+ TaskResult PythonBridge::evalExpressionSync(const std::string& expression) {
659
+ PyObject* mainModule = PyImport_AddModule("__main__");
660
+ if (!mainModule) return _makeErrorResult();
661
+
662
+ PyObject* globalDict = PyModule_GetDict(mainModule); // borrowed
663
+
664
+ PyObject* result = PyRun_String(expression.c_str(), Py_eval_input, globalDict, globalDict);
665
+ if (!result) {
666
+ return _makeErrorResult();
667
+ }
668
+
669
+ PyRef resultRef = PyRef::steal(result);
670
+ return _makeSuccessResult(result);
671
+ }
672
+
673
+ TaskResult PythonBridge::runFileSync(const std::string& filePath) {
674
+ FILE* fp = fopen(filePath.c_str(), "r");
675
+ if (!fp) {
676
+ TaskResult r;
677
+ r.success = false;
678
+ r.errorType = "FileNotFoundError";
679
+ r.errorMessage = "Cannot open file: " + filePath;
680
+ return r;
681
+ }
682
+
683
+ PyObject* mainModule = PyImport_AddModule("__main__");
684
+ PyObject* globalDict = PyModule_GetDict(mainModule);
685
+
686
+ PyObject* result = PyRun_File(fp, filePath.c_str(), Py_file_input, globalDict, globalDict);
687
+ fclose(fp);
688
+
689
+ if (!result) {
690
+ return _makeErrorResult();
691
+ }
692
+
693
+ PyRef resultRef = PyRef::steal(result);
694
+ TaskResult r;
695
+ r.success = true;
696
+ r.resultJson = "null";
697
+ return r;
698
+ }
699
+
700
+ // Object Operations
701
+
702
+ TaskResult PythonBridge::getObjectValueSync(uint32_t objectId) {
703
+ PyObject* obj = TypeConverter::getObject(objectId);
704
+ if (!obj) {
705
+ TaskResult r;
706
+ r.success = false;
707
+ r.errorType = "ValueError";
708
+ r.errorMessage = "Object not found: id=" + std::to_string(objectId);
709
+ return r;
710
+ }
711
+ return _makeSuccessResult(obj);
712
+ }
713
+
714
+ std::string PythonBridge::getObjectReprSync(uint32_t objectId) {
715
+ PyObject* obj = TypeConverter::getObject(objectId);
716
+ if (!obj) return "<deleted object>";
717
+ return TypeConverter::getRepr(obj);
718
+ }
719
+
720
+ TaskResult PythonBridge::getObjectLengthSync(uint32_t objectId) {
721
+ PyObject* obj = TypeConverter::getObject(objectId);
722
+ if (!obj) {
723
+ TaskResult r;
724
+ r.success = false;
725
+ r.errorType = "ValueError";
726
+ r.errorMessage = "Object not found";
727
+ return r;
728
+ }
729
+
730
+ Py_ssize_t len = PyObject_Length(obj);
731
+ if (len < 0) {
732
+ return _makeErrorResult();
733
+ }
734
+
735
+ TaskResult result;
736
+ result.success = true;
737
+ result.resultJson = std::to_string(len);
738
+ return result;
739
+ }
740
+
741
+ TaskResult PythonBridge::getObjectKeysSync(uint32_t objectId) {
742
+ PyObject* obj = TypeConverter::getObject(objectId);
743
+ if (!obj) {
744
+ TaskResult r;
745
+ r.success = false;
746
+ r.errorType = "ValueError";
747
+ r.errorMessage = "Object not found";
748
+ return r;
749
+ }
750
+
751
+ PyObject* dirList = PyObject_Dir(obj);
752
+ if (!dirList) {
753
+ return _makeErrorResult();
754
+ }
755
+ PyRef dirRef = PyRef::steal(dirList);
756
+
757
+ std::ostringstream oss;
758
+ oss << "[";
759
+ Py_ssize_t size = PyList_Size(dirList);
760
+ bool first = true;
761
+
762
+ for (Py_ssize_t i = 0; i < size; ++i) {
763
+ PyObject* name = PyList_GetItem(dirList, i);
764
+ if (!name || !PyUnicode_Check(name)) continue;
765
+
766
+ const char* nameStr = PyUnicode_AsUTF8(name);
767
+ if (!nameStr) continue;
768
+
769
+ // Filter out private names
770
+ if (nameStr[0] == '_') continue;
771
+
772
+ if (!first) oss << ",";
773
+ first = false;
774
+ oss << "\"" << nameStr << "\"";
775
+ }
776
+ oss << "]";
777
+
778
+ TaskResult result;
779
+ result.success = true;
780
+ result.resultJson = oss.str();
781
+ return result;
782
+ }
783
+
784
+ // Iterator Support
785
+
786
+ TaskResult PythonBridge::createIteratorSync(
787
+ uint32_t objectId,
788
+ const std::vector<std::string>& path
789
+ ) {
790
+ PyObject* obj = TypeConverter::getObject(objectId);
791
+ if (!obj) {
792
+ TaskResult r;
793
+ r.success = false;
794
+ r.errorType = "ValueError";
795
+ r.errorMessage = "Object not found";
796
+ return r;
797
+ }
798
+
799
+ // Resolve path
800
+ PyObject* target;
801
+ if (path.empty()) {
802
+ Py_INCREF(obj);
803
+ target = obj;
804
+ } else {
805
+ target = _resolveAttributePath(obj, path);
806
+ if (!target) return _makeErrorResult();
807
+ }
808
+ PyRef targetRef = PyRef::steal(target);
809
+
810
+ // Create iterator
811
+ PyObject* iter = PyObject_GetIter(target);
812
+ if (!iter) {
813
+ return _makeErrorResult();
814
+ }
815
+
816
+ uint32_t iteratorId = _nextIteratorId.fetch_add(1, std::memory_order_relaxed);
817
+ {
818
+ std::lock_guard<std::mutex> lock(_iteratorsMutex);
819
+ _iterators[iteratorId] = iter; // takes ownership
820
+ }
821
+
822
+ TaskResult result;
823
+ result.success = true;
824
+ result.objectId = iteratorId;
825
+ result.resultJson = "{\"iteratorId\":" + std::to_string(iteratorId) + "}";
826
+ return result;
827
+ }
828
+
829
+ TaskResult PythonBridge::iteratorNextSync(uint32_t iteratorId) {
830
+ PyObject* iter = nullptr;
831
+ {
832
+ std::lock_guard<std::mutex> lock(_iteratorsMutex);
833
+ auto it = _iterators.find(iteratorId);
834
+ if (it == _iterators.end()) {
835
+ TaskResult r;
836
+ r.success = false;
837
+ r.errorType = "ValueError";
838
+ r.errorMessage = "Iterator not found: id=" + std::to_string(iteratorId);
839
+ return r;
840
+ }
841
+ iter = it->second;
842
+ }
843
+
844
+ PyObject* next = PyIter_Next(iter);
845
+
846
+ if (!next) {
847
+ if (PyErr_ExceptionMatches(PyExc_StopIteration)) {
848
+ PyErr_Clear();
849
+ // Normal end of iteration
850
+ TaskResult result;
851
+ result.success = true;
852
+ result.resultJson = "{\"done\":true}";
853
+ return result;
854
+ }
855
+ return _makeErrorResult();
856
+ }
857
+
858
+ PyRef nextRef = PyRef::steal(next);
859
+ TaskResult itemResult = _makeSuccessResult(next);
860
+
861
+ if (itemResult.isObject) {
862
+ itemResult.resultJson = "{\"done\":false,\"value\":{\"__pyref__\":" +
863
+ std::to_string(itemResult.objectId) + "}}";
864
+ } else {
865
+ itemResult.resultJson = "{\"done\":false,\"value\":" + itemResult.resultJson + "}";
866
+ }
867
+
868
+ return itemResult;
869
+ }
870
+
871
+ void PythonBridge::destroyIteratorSync(uint32_t iteratorId) {
872
+ PyObject* iter = nullptr;
873
+ {
874
+ std::lock_guard<std::mutex> lock(_iteratorsMutex);
875
+ auto it = _iterators.find(iteratorId);
876
+ if (it != _iterators.end()) {
877
+ iter = it->second;
878
+ _iterators.erase(it);
879
+ }
880
+ }
881
+ Py_XDECREF(iter);
882
+ }
883
+
884
+ // Memory
885
+
886
+ void PythonBridge::collectGarbage() {
887
+ GILAcquire gil;
888
+ PyObject* gcModule = PyImport_ImportModule("gc");
889
+ if (gcModule) {
890
+ PyRef gcRef = PyRef::steal(gcModule);
891
+ PyObject* collect = PyObject_GetAttrString(gcModule, "collect");
892
+ if (collect) {
893
+ PyRef collectRef = PyRef::steal(collect);
894
+ PyObject* result = PyObject_CallNoArgs(collect);
895
+ Py_XDECREF(result);
896
+ } else { PyErr_Clear(); }
897
+ } else { PyErr_Clear(); }
898
+ }
899
+
900
+ size_t PythonBridge::getTrackedObjectCount() const {
901
+ return TypeConverter::getRegisteredCount();
902
+ }
903
+
904
+ } // namespace pynect