concurrent-c-node 0.14.0__tar.gz → 0.16.0__tar.gz
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.
- concurrent_c_node-0.16.0/PKG-INFO +192 -0
- concurrent_c_node-0.16.0/README.md +181 -0
- {concurrent_c_node-0.14.0 → concurrent_c_node-0.16.0}/cc_node/__init__.py +1 -1
- concurrent_c_node-0.16.0/cc_node/benchmarks/__init__.py +1 -0
- concurrent_c_node-0.16.0/cc_node/benchmarks/multi_domain.py +60 -0
- concurrent_c_node-0.16.0/concurrent_c_node.egg-info/PKG-INFO +192 -0
- {concurrent_c_node-0.14.0 → concurrent_c_node-0.16.0}/concurrent_c_node.egg-info/SOURCES.txt +2 -0
- {concurrent_c_node-0.14.0 → concurrent_c_node-0.16.0}/pyproject.toml +3 -2
- concurrent_c_node-0.14.0/PKG-INFO +0 -239
- concurrent_c_node-0.14.0/README.md +0 -228
- concurrent_c_node-0.14.0/concurrent_c_node.egg-info/PKG-INFO +0 -239
- {concurrent_c_node-0.14.0 → concurrent_c_node-0.16.0}/cc_node/broker.cjs +0 -0
- {concurrent_c_node-0.14.0 → concurrent_c_node-0.16.0}/cc_node/examples/__init__.py +0 -0
- {concurrent_c_node-0.14.0 → concurrent_c_node-0.16.0}/cc_node/examples/bench_wire.py +0 -0
- {concurrent_c_node-0.14.0 → concurrent_c_node-0.16.0}/cc_node/examples/use_node.py +0 -0
- {concurrent_c_node-0.14.0 → concurrent_c_node-0.16.0}/concurrent_c_node.egg-info/dependency_links.txt +0 -0
- {concurrent_c_node-0.14.0 → concurrent_c_node-0.16.0}/concurrent_c_node.egg-info/top_level.txt +0 -0
- {concurrent_c_node-0.14.0 → concurrent_c_node-0.16.0}/setup.cfg +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: concurrent-c-node
|
|
3
|
+
Version: 0.16.0
|
|
4
|
+
Summary: JavaScript and npm packages from Python over the Concurrent-C bridge: one spawned Node child per domain, host-controlled lifetime.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/sreekotay/concurrent-c
|
|
7
|
+
Project-URL: Documentation, https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/README.md
|
|
8
|
+
Keywords: javascript,node,npm,interop,concurrent-c
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# concurrent-c-node
|
|
13
|
+
|
|
14
|
+
Call Node (and npm packages) from Python.
|
|
15
|
+
Native types, exceptions, callbacks, and async all cross the boundary.
|
|
16
|
+
|
|
17
|
+
Part of [Concurrent-C](https://github.com/sreekotay/concurrent-c) — a
|
|
18
|
+
strict C11-superset preprocessor: `.ccs` lowers to plain C and compiles
|
|
19
|
+
with your host C compiler. (This bridge itself is pure Python stdlib —
|
|
20
|
+
no native build.)
|
|
21
|
+
|
|
22
|
+
Map of the three boundaries (CC hosts JS, native modules, this package
|
|
23
|
+
bridge):
|
|
24
|
+
[JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import cc_node
|
|
28
|
+
|
|
29
|
+
js = cc_node.create() # always a child `node` process
|
|
30
|
+
_ = js.require('lodash') # cwd node_modules
|
|
31
|
+
_.chunk([1, 2, 3, 4, 5], 2) # [[1, 2], [3, 4], [5]]
|
|
32
|
+
|
|
33
|
+
semver = js.require('semver')
|
|
34
|
+
semver.satisfies('1.2.3', '^1.0.0') # True
|
|
35
|
+
|
|
36
|
+
js.destroy() # or: with cc_node.create() as js:
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Unlike [`concurrent-c-python`](https://github.com/sreekotay/concurrent-c/tree/main/npm/cc-python)
|
|
40
|
+
(in-process by default), every `create()` here is a separate Node —
|
|
41
|
+
real addons, crash isolation, measurable wire. N domains = N processes.
|
|
42
|
+
|
|
43
|
+
| | this package | CC hosted (`cc_js_new(false, …)`) |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| API | `cc_node.create()` | `.ccs` program |
|
|
46
|
+
| Where | child `node` | libnode in-process |
|
|
47
|
+
| Hot call | ~105µs RTT | sub-µs (needs libnode) |
|
|
48
|
+
| Bulk | shm (~9.5ms / 8MB) | in-process |
|
|
49
|
+
| Parallelism | N children | one process |
|
|
50
|
+
| Crash | child dies; parent lives | shared fate |
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
pip install concurrent-c-node # needs node on PATH
|
|
54
|
+
python -m cc_node.examples.use_node
|
|
55
|
+
python -m cc_node.examples.bench_wire
|
|
56
|
+
python -m cc_node.benchmarks.multi_domain
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Measured
|
|
60
|
+
|
|
61
|
+
[`bench_wire`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/examples/bench_wire.py)
|
|
62
|
+
·
|
|
63
|
+
[`cc_node_bridge_py_20260810.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_bridge_py_20260810.txt):
|
|
64
|
+
|
|
65
|
+
| what | result |
|
|
66
|
+
|---|---|
|
|
67
|
+
| spawn (first eval) | 28ms |
|
|
68
|
+
| wire RTT | 105µs |
|
|
69
|
+
| Python callback round trip | 153µs |
|
|
70
|
+
| 8MB `array('d')` via shm | 9.5ms |
|
|
71
|
+
| same 8MB as JSON list | 499ms (~52×) |
|
|
72
|
+
|
|
73
|
+
Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
|
|
74
|
+
0700 dir, 0600 files, removed with the bridge. Crash isolation, not a
|
|
75
|
+
sandbox — don’t eval untrusted JS.
|
|
76
|
+
|
|
77
|
+
## Surface
|
|
78
|
+
|
|
79
|
+
- Plain data (numbers, str, bool, `None`, lists, non-empty dicts) by
|
|
80
|
+
value. Empty `{}` stays a live handle. Else: domain-owned handle
|
|
81
|
+
(attrs, calls, `str()` → `String()`). Non-finite floats are tagged.
|
|
82
|
+
- Handles stay in one domain. `stats()` / `release()` / idempotent
|
|
83
|
+
`destroy()`; after close: `bridge is closed`. Teardown is cooperative;
|
|
84
|
+
CPU-bound JS isn’t cancelable — wait or kill
|
|
85
|
+
([`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md)).
|
|
86
|
+
|
|
87
|
+
### Promises
|
|
88
|
+
|
|
89
|
+
Awaited in the child before the reply — no `async`/`await` on the
|
|
90
|
+
Python side:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
fetchish = js.eval('async (x) => { return { doubled: x * 2 } }')
|
|
94
|
+
fetchish(21) # {'doubled': 42}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Callbacks
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
mapped = js.eval('(f) => [1, 2, 3].map(f)')(lambda x, *rest: x * 10)
|
|
101
|
+
# map passes (value, index, array) — take *rest
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Exceptions cross both ways with messages intact.
|
|
105
|
+
|
|
106
|
+
### Buffers
|
|
107
|
+
|
|
108
|
+
`bytes` / `array.array` / 1-D numpy → typed arrays (and back). Small
|
|
109
|
+
inline; large via shm.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
import array
|
|
113
|
+
total = js.eval('(a) => a.reduce((s, x) => s + x, 0)')
|
|
114
|
+
total(array.array('d', range(1_000_000)))
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Common issues
|
|
118
|
+
|
|
119
|
+
**`Cannot find module`.** `require` / `import` resolve from the Python
|
|
120
|
+
process cwd (`node_modules` next to your program), not from this wheel’s
|
|
121
|
+
site-packages. `npm install lodash` in the project directory is the fix;
|
|
122
|
+
or `create(node=…)` / `CC_NODE_BIN` when the wrong Node is on `PATH`.
|
|
123
|
+
Missing-module errors name that cwd rule.
|
|
124
|
+
|
|
125
|
+
### Empty `{}` stays a handle
|
|
126
|
+
|
|
127
|
+
An empty object has to stay on the Node side — a materialized Python
|
|
128
|
+
`{}`/`dict` would lose later property use that matches Node. So
|
|
129
|
+
`js.eval('({})')` returns a live handle:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
o = js.eval('({})') # JsHandle, not {}
|
|
133
|
+
js.eval('(o) => { o.x = 1; return o.x }')(o) # 1
|
|
134
|
+
js.eval('({a: 1})') # {'a': 1} — data return
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Non-empty plain objects still cross as Python `dict`s. Same-domain
|
|
138
|
+
handles chain (`h.update(…).digest(…)`); foreign-domain handles do not.
|
|
139
|
+
|
|
140
|
+
**Thenables settle in the child.** Promise-based npm APIs need no
|
|
141
|
+
`async`/`await` on the Python side — the call blocks until settle (or
|
|
142
|
+
raises on reject). Opposite of `concurrent-c-python` isolated, where
|
|
143
|
+
every call is already a JS Promise you must await.
|
|
144
|
+
|
|
145
|
+
**Wire cost vs tiny work.** Round trip is ~100µs; a one-line JS helper
|
|
146
|
+
on three numbers loses to pure Python. Prefer Python (or a native CC
|
|
147
|
+
module) for small/hot work; use the bridge when Node/npm owns the kernel.
|
|
148
|
+
Multi-core: `python -m cc_node.benchmarks.multi_domain` (~2.8× on 3
|
|
149
|
+
domains here).
|
|
150
|
+
|
|
151
|
+
**Crash isolation, not a sandbox.** The child inherits your environment
|
|
152
|
+
— don’t eval untrusted JS. `destroy()` is cooperative; CPU-bound JS is
|
|
153
|
+
not preemptible (wait or kill + new domain).
|
|
154
|
+
|
|
155
|
+
## Choosing node
|
|
156
|
+
|
|
157
|
+
1. `create(node='/path/to/node')`
|
|
158
|
+
2. `CC_NODE_BIN`
|
|
159
|
+
3. `node` on `PATH`
|
|
160
|
+
|
|
161
|
+
Packages: cwd `node_modules`, same as Node itself.
|
|
162
|
+
|
|
163
|
+
From Concurrent-C (not Python): `cc_js_new(false, &a)` hosted
|
|
164
|
+
(libnode), or `cc_js_new(true, &a)` for this wire —
|
|
165
|
+
[`jsdemo.shcc`](https://github.com/sreekotay/concurrent-c/blob/main/examples/js/jsdemo.shcc).
|
|
166
|
+
|
|
167
|
+
## Publishing
|
|
168
|
+
|
|
169
|
+
**PyPI Trusted Publishing (OIDC)** — no API token:
|
|
170
|
+
|
|
171
|
+
1. [Publishing settings](https://pypi.org/manage/project/concurrent-c-node/settings/publishing/):
|
|
172
|
+
owner `sreekotay`, repo `concurrent-c`, workflow `publish-cc-node.yml`,
|
|
173
|
+
environment `pypi`
|
|
174
|
+
2. GitHub Environment `pypi`
|
|
175
|
+
3. Bump `pyproject.toml`, push, then:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
gh workflow run publish-cc-node.yml
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Local / npm sibling:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
./scripts/publish_bridges.sh --publish --minor
|
|
185
|
+
gh workflow run publish-cc-node.yml
|
|
186
|
+
# twine fallback: … --pypi-twine
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`.
|
|
190
|
+
Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
|
|
191
|
+
Own hot path in C/CC → native module (40–90ns) instead of the wire —
|
|
192
|
+
[JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# concurrent-c-node
|
|
2
|
+
|
|
3
|
+
Call Node (and npm packages) from Python.
|
|
4
|
+
Native types, exceptions, callbacks, and async all cross the boundary.
|
|
5
|
+
|
|
6
|
+
Part of [Concurrent-C](https://github.com/sreekotay/concurrent-c) — a
|
|
7
|
+
strict C11-superset preprocessor: `.ccs` lowers to plain C and compiles
|
|
8
|
+
with your host C compiler. (This bridge itself is pure Python stdlib —
|
|
9
|
+
no native build.)
|
|
10
|
+
|
|
11
|
+
Map of the three boundaries (CC hosts JS, native modules, this package
|
|
12
|
+
bridge):
|
|
13
|
+
[JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import cc_node
|
|
17
|
+
|
|
18
|
+
js = cc_node.create() # always a child `node` process
|
|
19
|
+
_ = js.require('lodash') # cwd node_modules
|
|
20
|
+
_.chunk([1, 2, 3, 4, 5], 2) # [[1, 2], [3, 4], [5]]
|
|
21
|
+
|
|
22
|
+
semver = js.require('semver')
|
|
23
|
+
semver.satisfies('1.2.3', '^1.0.0') # True
|
|
24
|
+
|
|
25
|
+
js.destroy() # or: with cc_node.create() as js:
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Unlike [`concurrent-c-python`](https://github.com/sreekotay/concurrent-c/tree/main/npm/cc-python)
|
|
29
|
+
(in-process by default), every `create()` here is a separate Node —
|
|
30
|
+
real addons, crash isolation, measurable wire. N domains = N processes.
|
|
31
|
+
|
|
32
|
+
| | this package | CC hosted (`cc_js_new(false, …)`) |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| API | `cc_node.create()` | `.ccs` program |
|
|
35
|
+
| Where | child `node` | libnode in-process |
|
|
36
|
+
| Hot call | ~105µs RTT | sub-µs (needs libnode) |
|
|
37
|
+
| Bulk | shm (~9.5ms / 8MB) | in-process |
|
|
38
|
+
| Parallelism | N children | one process |
|
|
39
|
+
| Crash | child dies; parent lives | shared fate |
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
pip install concurrent-c-node # needs node on PATH
|
|
43
|
+
python -m cc_node.examples.use_node
|
|
44
|
+
python -m cc_node.examples.bench_wire
|
|
45
|
+
python -m cc_node.benchmarks.multi_domain
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Measured
|
|
49
|
+
|
|
50
|
+
[`bench_wire`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/examples/bench_wire.py)
|
|
51
|
+
·
|
|
52
|
+
[`cc_node_bridge_py_20260810.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_bridge_py_20260810.txt):
|
|
53
|
+
|
|
54
|
+
| what | result |
|
|
55
|
+
|---|---|
|
|
56
|
+
| spawn (first eval) | 28ms |
|
|
57
|
+
| wire RTT | 105µs |
|
|
58
|
+
| Python callback round trip | 153µs |
|
|
59
|
+
| 8MB `array('d')` via shm | 9.5ms |
|
|
60
|
+
| same 8MB as JSON list | 499ms (~52×) |
|
|
61
|
+
|
|
62
|
+
Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
|
|
63
|
+
0700 dir, 0600 files, removed with the bridge. Crash isolation, not a
|
|
64
|
+
sandbox — don’t eval untrusted JS.
|
|
65
|
+
|
|
66
|
+
## Surface
|
|
67
|
+
|
|
68
|
+
- Plain data (numbers, str, bool, `None`, lists, non-empty dicts) by
|
|
69
|
+
value. Empty `{}` stays a live handle. Else: domain-owned handle
|
|
70
|
+
(attrs, calls, `str()` → `String()`). Non-finite floats are tagged.
|
|
71
|
+
- Handles stay in one domain. `stats()` / `release()` / idempotent
|
|
72
|
+
`destroy()`; after close: `bridge is closed`. Teardown is cooperative;
|
|
73
|
+
CPU-bound JS isn’t cancelable — wait or kill
|
|
74
|
+
([`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md)).
|
|
75
|
+
|
|
76
|
+
### Promises
|
|
77
|
+
|
|
78
|
+
Awaited in the child before the reply — no `async`/`await` on the
|
|
79
|
+
Python side:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
fetchish = js.eval('async (x) => { return { doubled: x * 2 } }')
|
|
83
|
+
fetchish(21) # {'doubled': 42}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Callbacks
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
mapped = js.eval('(f) => [1, 2, 3].map(f)')(lambda x, *rest: x * 10)
|
|
90
|
+
# map passes (value, index, array) — take *rest
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Exceptions cross both ways with messages intact.
|
|
94
|
+
|
|
95
|
+
### Buffers
|
|
96
|
+
|
|
97
|
+
`bytes` / `array.array` / 1-D numpy → typed arrays (and back). Small
|
|
98
|
+
inline; large via shm.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
import array
|
|
102
|
+
total = js.eval('(a) => a.reduce((s, x) => s + x, 0)')
|
|
103
|
+
total(array.array('d', range(1_000_000)))
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Common issues
|
|
107
|
+
|
|
108
|
+
**`Cannot find module`.** `require` / `import` resolve from the Python
|
|
109
|
+
process cwd (`node_modules` next to your program), not from this wheel’s
|
|
110
|
+
site-packages. `npm install lodash` in the project directory is the fix;
|
|
111
|
+
or `create(node=…)` / `CC_NODE_BIN` when the wrong Node is on `PATH`.
|
|
112
|
+
Missing-module errors name that cwd rule.
|
|
113
|
+
|
|
114
|
+
### Empty `{}` stays a handle
|
|
115
|
+
|
|
116
|
+
An empty object has to stay on the Node side — a materialized Python
|
|
117
|
+
`{}`/`dict` would lose later property use that matches Node. So
|
|
118
|
+
`js.eval('({})')` returns a live handle:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
o = js.eval('({})') # JsHandle, not {}
|
|
122
|
+
js.eval('(o) => { o.x = 1; return o.x }')(o) # 1
|
|
123
|
+
js.eval('({a: 1})') # {'a': 1} — data return
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Non-empty plain objects still cross as Python `dict`s. Same-domain
|
|
127
|
+
handles chain (`h.update(…).digest(…)`); foreign-domain handles do not.
|
|
128
|
+
|
|
129
|
+
**Thenables settle in the child.** Promise-based npm APIs need no
|
|
130
|
+
`async`/`await` on the Python side — the call blocks until settle (or
|
|
131
|
+
raises on reject). Opposite of `concurrent-c-python` isolated, where
|
|
132
|
+
every call is already a JS Promise you must await.
|
|
133
|
+
|
|
134
|
+
**Wire cost vs tiny work.** Round trip is ~100µs; a one-line JS helper
|
|
135
|
+
on three numbers loses to pure Python. Prefer Python (or a native CC
|
|
136
|
+
module) for small/hot work; use the bridge when Node/npm owns the kernel.
|
|
137
|
+
Multi-core: `python -m cc_node.benchmarks.multi_domain` (~2.8× on 3
|
|
138
|
+
domains here).
|
|
139
|
+
|
|
140
|
+
**Crash isolation, not a sandbox.** The child inherits your environment
|
|
141
|
+
— don’t eval untrusted JS. `destroy()` is cooperative; CPU-bound JS is
|
|
142
|
+
not preemptible (wait or kill + new domain).
|
|
143
|
+
|
|
144
|
+
## Choosing node
|
|
145
|
+
|
|
146
|
+
1. `create(node='/path/to/node')`
|
|
147
|
+
2. `CC_NODE_BIN`
|
|
148
|
+
3. `node` on `PATH`
|
|
149
|
+
|
|
150
|
+
Packages: cwd `node_modules`, same as Node itself.
|
|
151
|
+
|
|
152
|
+
From Concurrent-C (not Python): `cc_js_new(false, &a)` hosted
|
|
153
|
+
(libnode), or `cc_js_new(true, &a)` for this wire —
|
|
154
|
+
[`jsdemo.shcc`](https://github.com/sreekotay/concurrent-c/blob/main/examples/js/jsdemo.shcc).
|
|
155
|
+
|
|
156
|
+
## Publishing
|
|
157
|
+
|
|
158
|
+
**PyPI Trusted Publishing (OIDC)** — no API token:
|
|
159
|
+
|
|
160
|
+
1. [Publishing settings](https://pypi.org/manage/project/concurrent-c-node/settings/publishing/):
|
|
161
|
+
owner `sreekotay`, repo `concurrent-c`, workflow `publish-cc-node.yml`,
|
|
162
|
+
environment `pypi`
|
|
163
|
+
2. GitHub Environment `pypi`
|
|
164
|
+
3. Bump `pyproject.toml`, push, then:
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
gh workflow run publish-cc-node.yml
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Local / npm sibling:
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
./scripts/publish_bridges.sh --publish --minor
|
|
174
|
+
gh workflow run publish-cc-node.yml
|
|
175
|
+
# twine fallback: … --pypi-twine
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`.
|
|
179
|
+
Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
|
|
180
|
+
Own hot path in C/CC → native module (40–90ns) instead of the wire —
|
|
181
|
+
[JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
|
|
@@ -30,7 +30,7 @@ import subprocess
|
|
|
30
30
|
import tempfile
|
|
31
31
|
|
|
32
32
|
__all__ = ["create", "JsError", "JsHandle", "__version__"]
|
|
33
|
-
__version__ = "0.
|
|
33
|
+
__version__ = "0.13.0"
|
|
34
34
|
|
|
35
35
|
# Typed buffers cross as typed arrays; big ones spill through shared
|
|
36
36
|
# memory (tmpfs where available) — one memcpy per side, receiver
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Measurement scripts for concurrent-c-node (RESULT lines, machine-comparable).
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""N node children on N cores — measured with real overlap.
|
|
2
|
+
|
|
3
|
+
python -m cc_node.benchmarks.multi_domain
|
|
4
|
+
|
|
5
|
+
Wire RTT / shm live in ``cc_node.examples.bench_wire``. This file only
|
|
6
|
+
answers the multi-domain question: sequential calls on one child vs the
|
|
7
|
+
same work fanned across threads into separate children.
|
|
8
|
+
"""
|
|
9
|
+
import time
|
|
10
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
11
|
+
|
|
12
|
+
import cc_node
|
|
13
|
+
|
|
14
|
+
WORK = """
|
|
15
|
+
(iterations) => {
|
|
16
|
+
let s = 0;
|
|
17
|
+
for (let i = 0; i < iterations; i++)
|
|
18
|
+
s += Math.sqrt(i) * Math.sin(i / 1000);
|
|
19
|
+
return s;
|
|
20
|
+
}
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main():
|
|
25
|
+
n_dom = 3
|
|
26
|
+
iters = 40_000_000
|
|
27
|
+
|
|
28
|
+
with cc_node.create() as js:
|
|
29
|
+
f = js.eval(WORK)
|
|
30
|
+
f(iters // 20)
|
|
31
|
+
t0 = time.perf_counter()
|
|
32
|
+
for _ in range(n_dom):
|
|
33
|
+
f(iters)
|
|
34
|
+
seq = time.perf_counter() - t0
|
|
35
|
+
print("RESULT seq_s %.3f" % seq)
|
|
36
|
+
|
|
37
|
+
domains = [cc_node.create() for _ in range(n_dom)]
|
|
38
|
+
try:
|
|
39
|
+
fns = [d.eval(WORK) for d in domains]
|
|
40
|
+
for f in fns:
|
|
41
|
+
f(iters // 20)
|
|
42
|
+
|
|
43
|
+
def run(f):
|
|
44
|
+
return f(iters)
|
|
45
|
+
|
|
46
|
+
t0 = time.perf_counter()
|
|
47
|
+
with ThreadPoolExecutor(max_workers=n_dom) as pool:
|
|
48
|
+
list(pool.map(run, fns))
|
|
49
|
+
par = time.perf_counter() - t0
|
|
50
|
+
finally:
|
|
51
|
+
for d in domains:
|
|
52
|
+
d.destroy()
|
|
53
|
+
|
|
54
|
+
print("RESULT par_s %.3f" % par)
|
|
55
|
+
print("RESULT speedup %.2f" % (seq / par if par else 0))
|
|
56
|
+
print("done")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
main()
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: concurrent-c-node
|
|
3
|
+
Version: 0.16.0
|
|
4
|
+
Summary: JavaScript and npm packages from Python over the Concurrent-C bridge: one spawned Node child per domain, host-controlled lifetime.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/sreekotay/concurrent-c
|
|
7
|
+
Project-URL: Documentation, https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/README.md
|
|
8
|
+
Keywords: javascript,node,npm,interop,concurrent-c
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# concurrent-c-node
|
|
13
|
+
|
|
14
|
+
Call Node (and npm packages) from Python.
|
|
15
|
+
Native types, exceptions, callbacks, and async all cross the boundary.
|
|
16
|
+
|
|
17
|
+
Part of [Concurrent-C](https://github.com/sreekotay/concurrent-c) — a
|
|
18
|
+
strict C11-superset preprocessor: `.ccs` lowers to plain C and compiles
|
|
19
|
+
with your host C compiler. (This bridge itself is pure Python stdlib —
|
|
20
|
+
no native build.)
|
|
21
|
+
|
|
22
|
+
Map of the three boundaries (CC hosts JS, native modules, this package
|
|
23
|
+
bridge):
|
|
24
|
+
[JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import cc_node
|
|
28
|
+
|
|
29
|
+
js = cc_node.create() # always a child `node` process
|
|
30
|
+
_ = js.require('lodash') # cwd node_modules
|
|
31
|
+
_.chunk([1, 2, 3, 4, 5], 2) # [[1, 2], [3, 4], [5]]
|
|
32
|
+
|
|
33
|
+
semver = js.require('semver')
|
|
34
|
+
semver.satisfies('1.2.3', '^1.0.0') # True
|
|
35
|
+
|
|
36
|
+
js.destroy() # or: with cc_node.create() as js:
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Unlike [`concurrent-c-python`](https://github.com/sreekotay/concurrent-c/tree/main/npm/cc-python)
|
|
40
|
+
(in-process by default), every `create()` here is a separate Node —
|
|
41
|
+
real addons, crash isolation, measurable wire. N domains = N processes.
|
|
42
|
+
|
|
43
|
+
| | this package | CC hosted (`cc_js_new(false, …)`) |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| API | `cc_node.create()` | `.ccs` program |
|
|
46
|
+
| Where | child `node` | libnode in-process |
|
|
47
|
+
| Hot call | ~105µs RTT | sub-µs (needs libnode) |
|
|
48
|
+
| Bulk | shm (~9.5ms / 8MB) | in-process |
|
|
49
|
+
| Parallelism | N children | one process |
|
|
50
|
+
| Crash | child dies; parent lives | shared fate |
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
pip install concurrent-c-node # needs node on PATH
|
|
54
|
+
python -m cc_node.examples.use_node
|
|
55
|
+
python -m cc_node.examples.bench_wire
|
|
56
|
+
python -m cc_node.benchmarks.multi_domain
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Measured
|
|
60
|
+
|
|
61
|
+
[`bench_wire`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/examples/bench_wire.py)
|
|
62
|
+
·
|
|
63
|
+
[`cc_node_bridge_py_20260810.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_bridge_py_20260810.txt):
|
|
64
|
+
|
|
65
|
+
| what | result |
|
|
66
|
+
|---|---|
|
|
67
|
+
| spawn (first eval) | 28ms |
|
|
68
|
+
| wire RTT | 105µs |
|
|
69
|
+
| Python callback round trip | 153µs |
|
|
70
|
+
| 8MB `array('d')` via shm | 9.5ms |
|
|
71
|
+
| same 8MB as JSON list | 499ms (~52×) |
|
|
72
|
+
|
|
73
|
+
Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
|
|
74
|
+
0700 dir, 0600 files, removed with the bridge. Crash isolation, not a
|
|
75
|
+
sandbox — don’t eval untrusted JS.
|
|
76
|
+
|
|
77
|
+
## Surface
|
|
78
|
+
|
|
79
|
+
- Plain data (numbers, str, bool, `None`, lists, non-empty dicts) by
|
|
80
|
+
value. Empty `{}` stays a live handle. Else: domain-owned handle
|
|
81
|
+
(attrs, calls, `str()` → `String()`). Non-finite floats are tagged.
|
|
82
|
+
- Handles stay in one domain. `stats()` / `release()` / idempotent
|
|
83
|
+
`destroy()`; after close: `bridge is closed`. Teardown is cooperative;
|
|
84
|
+
CPU-bound JS isn’t cancelable — wait or kill
|
|
85
|
+
([`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md)).
|
|
86
|
+
|
|
87
|
+
### Promises
|
|
88
|
+
|
|
89
|
+
Awaited in the child before the reply — no `async`/`await` on the
|
|
90
|
+
Python side:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
fetchish = js.eval('async (x) => { return { doubled: x * 2 } }')
|
|
94
|
+
fetchish(21) # {'doubled': 42}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Callbacks
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
mapped = js.eval('(f) => [1, 2, 3].map(f)')(lambda x, *rest: x * 10)
|
|
101
|
+
# map passes (value, index, array) — take *rest
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Exceptions cross both ways with messages intact.
|
|
105
|
+
|
|
106
|
+
### Buffers
|
|
107
|
+
|
|
108
|
+
`bytes` / `array.array` / 1-D numpy → typed arrays (and back). Small
|
|
109
|
+
inline; large via shm.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
import array
|
|
113
|
+
total = js.eval('(a) => a.reduce((s, x) => s + x, 0)')
|
|
114
|
+
total(array.array('d', range(1_000_000)))
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Common issues
|
|
118
|
+
|
|
119
|
+
**`Cannot find module`.** `require` / `import` resolve from the Python
|
|
120
|
+
process cwd (`node_modules` next to your program), not from this wheel’s
|
|
121
|
+
site-packages. `npm install lodash` in the project directory is the fix;
|
|
122
|
+
or `create(node=…)` / `CC_NODE_BIN` when the wrong Node is on `PATH`.
|
|
123
|
+
Missing-module errors name that cwd rule.
|
|
124
|
+
|
|
125
|
+
### Empty `{}` stays a handle
|
|
126
|
+
|
|
127
|
+
An empty object has to stay on the Node side — a materialized Python
|
|
128
|
+
`{}`/`dict` would lose later property use that matches Node. So
|
|
129
|
+
`js.eval('({})')` returns a live handle:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
o = js.eval('({})') # JsHandle, not {}
|
|
133
|
+
js.eval('(o) => { o.x = 1; return o.x }')(o) # 1
|
|
134
|
+
js.eval('({a: 1})') # {'a': 1} — data return
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Non-empty plain objects still cross as Python `dict`s. Same-domain
|
|
138
|
+
handles chain (`h.update(…).digest(…)`); foreign-domain handles do not.
|
|
139
|
+
|
|
140
|
+
**Thenables settle in the child.** Promise-based npm APIs need no
|
|
141
|
+
`async`/`await` on the Python side — the call blocks until settle (or
|
|
142
|
+
raises on reject). Opposite of `concurrent-c-python` isolated, where
|
|
143
|
+
every call is already a JS Promise you must await.
|
|
144
|
+
|
|
145
|
+
**Wire cost vs tiny work.** Round trip is ~100µs; a one-line JS helper
|
|
146
|
+
on three numbers loses to pure Python. Prefer Python (or a native CC
|
|
147
|
+
module) for small/hot work; use the bridge when Node/npm owns the kernel.
|
|
148
|
+
Multi-core: `python -m cc_node.benchmarks.multi_domain` (~2.8× on 3
|
|
149
|
+
domains here).
|
|
150
|
+
|
|
151
|
+
**Crash isolation, not a sandbox.** The child inherits your environment
|
|
152
|
+
— don’t eval untrusted JS. `destroy()` is cooperative; CPU-bound JS is
|
|
153
|
+
not preemptible (wait or kill + new domain).
|
|
154
|
+
|
|
155
|
+
## Choosing node
|
|
156
|
+
|
|
157
|
+
1. `create(node='/path/to/node')`
|
|
158
|
+
2. `CC_NODE_BIN`
|
|
159
|
+
3. `node` on `PATH`
|
|
160
|
+
|
|
161
|
+
Packages: cwd `node_modules`, same as Node itself.
|
|
162
|
+
|
|
163
|
+
From Concurrent-C (not Python): `cc_js_new(false, &a)` hosted
|
|
164
|
+
(libnode), or `cc_js_new(true, &a)` for this wire —
|
|
165
|
+
[`jsdemo.shcc`](https://github.com/sreekotay/concurrent-c/blob/main/examples/js/jsdemo.shcc).
|
|
166
|
+
|
|
167
|
+
## Publishing
|
|
168
|
+
|
|
169
|
+
**PyPI Trusted Publishing (OIDC)** — no API token:
|
|
170
|
+
|
|
171
|
+
1. [Publishing settings](https://pypi.org/manage/project/concurrent-c-node/settings/publishing/):
|
|
172
|
+
owner `sreekotay`, repo `concurrent-c`, workflow `publish-cc-node.yml`,
|
|
173
|
+
environment `pypi`
|
|
174
|
+
2. GitHub Environment `pypi`
|
|
175
|
+
3. Bump `pyproject.toml`, push, then:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
gh workflow run publish-cc-node.yml
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Local / npm sibling:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
./scripts/publish_bridges.sh --publish --minor
|
|
185
|
+
gh workflow run publish-cc-node.yml
|
|
186
|
+
# twine fallback: … --pypi-twine
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`.
|
|
190
|
+
Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
|
|
191
|
+
Own hot path in C/CC → native module (40–90ns) instead of the wire —
|
|
192
|
+
[JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "concurrent-c-node"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.16.0"
|
|
8
8
|
description = "JavaScript and npm packages from Python over the Concurrent-C bridge: one spawned Node child per domain, host-controlled lifetime."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -16,8 +16,9 @@ Repository = "https://github.com/sreekotay/concurrent-c"
|
|
|
16
16
|
Documentation = "https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/README.md"
|
|
17
17
|
|
|
18
18
|
[tool.setuptools]
|
|
19
|
-
packages = ["cc_node", "cc_node.examples"]
|
|
19
|
+
packages = ["cc_node", "cc_node.examples", "cc_node.benchmarks"]
|
|
20
20
|
|
|
21
21
|
[tool.setuptools.package-data]
|
|
22
22
|
cc_node = ["broker.cjs"]
|
|
23
23
|
"cc_node.examples" = ["*.py"]
|
|
24
|
+
"cc_node.benchmarks" = ["*.py"]
|