concurrent-c-node
JavaScript — and every npm package — from Python.
Part of Concurrent-C — a
strict C11-superset preprocessor: .ccs lowers to plain C and compiles
with your host C compiler. (This bridge itself is pure Python.)
Map of the three boundaries (CC hosts Python, native modules, this package bridge): JS / Python interop.
import cc_node
js = cc_node.create() # always a SEPARATE node process
_ = js.require('lodash') # resolved from YOUR cwd's node_modules
_.chunk([1, 2, 3, 4, 5], 2) # [[1, 2], [3, 4], [5]]
_.sortBy([{'n': 3}, {'n': 1}], 'n') # dicts cross as objects, and back
semver = js.require('semver')
semver.satisfies('1.2.3', '^1.0.0') # True
js.destroy() # or: with cc_node.create() as js: ...
Separate process by design
Unlike concurrent-c-python
(whose default embeds libpython in the Node process, with
{ isolated: true } as the child-process opt-in), every
cc_node.create() is already the isolated tier: one spawned node
child per domain. There is no in-process Node embed from Python —
you get real Node (full stdlib, native addons, whatever npm install
put next to your program), crash isolation, and a wire you can measure.
N domains are N OS processes: full multi-core speedup — fan work
across create() handles and they run on separate cores, no shared
event-loop or GIL between them.
this package — separate node process |
Concurrent-C hosted (not this wheel) | |
|---|---|---|
| API | cc_node.create() from Python |
cc_js_new(false, &a) in a .ccs program |
| Where JS runs | own node child |
libnode in the CC process |
| Hot call | ~105µs wire RTT | sub-µs (needs libnode-dev) |
| Bulk buffers | shm spill — 8MB in 9.5ms (52× a JSON list) | in-process |
| Parallelism | N children = N cores — full multi-core speedup | one process (V8's rule) |
| Crash | child dies → error; Python parent lives | shared fate with the host |
pip install concurrent-c-node # needs node on PATH (or point at one)
python -m cc_node.examples.use_node
python -m cc_node.examples.bench_wire
python -m cc_node.benchmarks.multi_domain # N children, thread-fanned
Measured (separate-process wire)
From python -m cc_node.examples.bench_wire (sources under
cc_node/examples/)
on a 4-vCPU x86-64 box, node 22 / python 3.11
(perf/baselines/cc_node_bridge_py_20260810.txt;
catalog: perf/baselines/README.md):
| what | result |
|---|---|
| spawn a domain (node child, first eval) | 28ms |
| wire round trip (smallest call) | 105µs |
| Python-callback round trip (JS → Python → JS) | 153µs |
8MB array('d') argument, shm spill |
9.5ms |
| the same 8MB as a JSON list | 499ms — the spill is 52x |
The wire is strict request/response JSON on dedicated fds — replies
pair by request id, and stdio stays yours, so console.log in
evaluated JS reaches the real stdout and can never collide with a
protocol reply — with the shared-memory spill for bulk data (private
0700 per-bridge directory, 0600 exclusive-create files, removed with
the bridge). The same discipline concurrent-c-python's
{ isolated: true } domains speak, mirrored.
One boundary, stated plainly: the domain is crash isolation, not a security sandbox — the node child inherits your environment and runs with your OS privileges, so do not run untrusted JavaScript through it.
The bridge is pure Python, stdlib only — no compiled code, no
dependencies, nothing to build. Import stays import cc_node.
Examples ship in the wheel. Same domain model and materialization
rules as the npm sibling, pointed the other way:
- Values: plain data (finite numbers, strings, booleans,
None, lists and non-empty dicts/objects of the same) crosses by value; an empty{}stays a live handle (so bags you mint in JS keep property access). Everything else is a live handle owned by the domain — attribute access is property lookup (methods arrive bound), calls are calls,str()isString(). Non-finite floats cross tagged, never silently nulled. - The domain rules hold: handles never cross bridges;
stats()is the handle ledger andrelease()drops one early;destroy()is idempotent, every door answersbridge is closedafter, and the child dies with the bridge (and on host exit, via wire-fd EOF). Teardown is cooperative (farewellclose+ drain, then wait / kill-fallback): in-flight calls may still return a correct value. There is no clean cancel of CPU-bound JS work — wait, or kill the child (SIGKILL/process.abort) and create a new domain. Hard death must reject in-flight ops. Seebridge_stress.md.
Async is free
A thenable result is awaited in the child before the reply, so
promise-based package APIs need nothing special — no event loop on the
Python side, no await:
fetchish = js.eval('async (x) => { return { doubled: x * 2 } }')
fetchish(21) # {'doubled': 42} — just a call
Whatever an npm package's API returns — value or promise — the call site reads the same.
Callbacks: Python functions as JS functions
A Python callable passed as an argument crosses as a JS function, and may be called back any number of times — including from inside async JS code:
mapped = js.eval('(f) => [1, 2, 3].map(f)')(lambda x, *rest: x * 10)
# [10, 20, 30] — JS conventions apply: map passes (value, index, array),
# so a lambda takes *rest. Exceptions cross both ways, messages intact.
Nested callbacks compose (the wire alternates strictly), and a Python exception inside one surfaces as the JS error at the call site — and vice versa.
Buffers: typed arrays, shared memory
bytes, array.array, and 1-D numpy arrays cross as
Float64Array / Int32Array / Uint8Array / … and come back as numpy
arrays (or array.array without numpy):
import array
total = js.eval('(a) => a.reduce((s, x) => s + x, 0)')
total(array.array('d', range(1_000_000))) # crosses via shared memory
Small buffers inline; big ones spill through shared memory — one memcpy per side, the receiver consumes the spill file, and the sender sweeps it if the child died first. Nothing strays, and nothing is silently truncated: an unsupported type is an articulate error.
Common issues
Cannot find module '…'. require / import resolve from the
Python process cwd (node_modules next to your program), not from
the site-packages install of this wheel. npm install lodash in the
project directory is the fix; or pass create(node='/path/to/node') /
CC_NODE_BIN when the wrong Node is on PATH. Missing-module errors
name that cwd rule.
Empty {} is a live handle. js.eval('({})') stays a JsHandle
so later property use matches Node. Non-empty plain objects still cross
as Python dicts (data returns). Same-domain handles chain
(h.update(…).digest(…)); foreign-domain handles do not.
Thenables are awaited in the child. Promise-based npm APIs need no
async/await on the Python side — the call blocks until settle (or
raises JsError on reject). That is the opposite of
concurrent-c-python's isolated surface, where every call is already a
JS Promise you must await.
Wire cost vs tiny work. Round trip is ~100µs class; a one-line JS
helper on three numbers loses to pure Python. Prefer Python (or a native
CC module) for small/hot work; use the bridge when Node/npm owns the
kernel (crypto, parsers, large buffers via shm). Multi-core: fan across
create() handles from threads —
python -m cc_node.benchmarks.multi_domain (~2.8× on 3 domains here).
Crash isolation, not a sandbox. The child inherits your environment
and privileges — do not evaluate untrusted JavaScript. destroy() is
cooperative; CPU-bound JS is not preemptible (wait or kill + new domain).
Choosing the node
Same ambient-first rule as the rest of the family: the domain runs
whatever node your project runs.
create(node='/path/to/node')from code — per-domain.CC_NODE_BINin the environment.nodeonPATH.
And which packages it sees is the working directory's
node_modules — require resolves exactly as node itself would there.
Run Python in your project, get your project's packages: npm install
next to your program is the whole setup.
Writing Concurrent-C itself rather than Python? The zero-IPC hosted
tier is cc_js_new(false, &a) (needs libnode) —
examples/js/jsdemo.shcc;
cc_js_new(true, &a) is the same separate-process wire this package
speaks, from CC.
Publishing
Preferred — PyPI Trusted Publishing (OIDC) from CI (no API token):
- One-time on
Publishing settings:
- Owner
sreekotay, repositoryconcurrent-c - Workflow name
publish-cc-node.yml - Environment name
pypi
- Owner
- Create a GitHub Environment named
pypi(optional reviewers encouraged). - Bump
versioninpyproject.toml, commit + push, then:
gh workflow run publish-cc-node.yml
# or: git tag cc-node-vX.Y.Z && git push --tags
Local pack / npm sibling (PyPI defaults to CI OIDC after npm):
./scripts/publish_bridges.sh --publish --minor
# npm is live; then commit+push bumps and:
gh workflow run publish-cc-node.yml
# local twine fallback:
./scripts/publish_bridges.sh --publish --minor --pypi-twine
A worked tour (builtin Node modules, chains, callbacks, thenables,
buffers — no npm install needed):
python -m cc_node.examples.use_node. Wire RTT / shm:
python -m cc_node.examples.bench_wire. Multi-core domains (threads):
python -m cc_node.benchmarks.multi_domain.
Adversarial multi-child storm (escaped closures, cooperative
fanout-destroy, abort inject, handle-leak / RSS soaks):
stress/bridge/
— ./stress/bridge/run.sh (CHAOS_SCALE=full / soak for bigger N).
Mode catalog + destroy contracts:
bridge_stress.md
(latency demos stay in cc_node/examples/).
And when the hot path is YOUR code rather than an npm package, skip the wire entirely: a page of Concurrent-C (or C) exports as a native module for Python and Node both — 40-90ns calls, stable-ABI artifacts. See Native modules for Node and Python.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file concurrent_c_node-0.15.0.tar.gz.
File metadata
- Download URL: concurrent_c_node-0.15.0.tar.gz
- Upload date:
- Size: 20.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
81c9d128d83ec430251874427482f5a1a075ffe3fde942f7580b282b8ca9f33e
|
|
| MD5 |
add0ff193d6577f7ef94d6c50cdb430e
|
|
| BLAKE2b-256 |
7fac5616fd464bc03ad2bf335ea38b18548b9320677b4c2bc44de7d0de745d7e
|
Provenance
The following attestation bundles were made for concurrent_c_node-0.15.0.tar.gz:
Publisher:
publish-cc-node.yml on sreekotay/concurrent-c
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
concurrent_c_node-0.15.0.tar.gz -
Subject digest:
81c9d128d83ec430251874427482f5a1a075ffe3fde942f7580b282b8ca9f33e - Sigstore transparency entry: 2414431900
- Sigstore integration time:
-
Permalink:
sreekotay/concurrent-c@59287ce389f4b250a999e46f8cf2b79c19efecd7 -
Branch / Tag:
refs/tags/cc-node-v0.15.0 - Owner: https://github.com/sreekotay
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-cc-node.yml@59287ce389f4b250a999e46f8cf2b79c19efecd7 -
Trigger Event:
push
-
Statement type:
File details
Details for the file concurrent_c_node-0.15.0-py3-none-any.whl.
File metadata
- Download URL: concurrent_c_node-0.15.0-py3-none-any.whl
- Upload date:
- Size: 17.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
44519e14d7d74d16a6ff0587c5a487ab50610b88f64e602acb66aa143c96d527
|
|
| MD5 |
1101f26970fbcde188a4ce14292b7dc6
|
|
| BLAKE2b-256 |
2e0545a692670a97bed3e6b91bca1b25334fbcc357ae4cbe397e2edcc0ed2875
|
Provenance
The following attestation bundles were made for concurrent_c_node-0.15.0-py3-none-any.whl:
Publisher:
publish-cc-node.yml on sreekotay/concurrent-c
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
concurrent_c_node-0.15.0-py3-none-any.whl -
Subject digest:
44519e14d7d74d16a6ff0587c5a487ab50610b88f64e602acb66aa143c96d527 - Sigstore transparency entry: 2414432008
- Sigstore integration time:
-
Permalink:
sreekotay/concurrent-c@59287ce389f4b250a999e46f8cf2b79c19efecd7 -
Branch / Tag:
refs/tags/cc-node-v0.15.0 - Owner: https://github.com/sreekotay
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-cc-node.yml@59287ce389f4b250a999e46f8cf2b79c19efecd7 -
Trigger Event:
push
-
Statement type: