A single Go binary that embeds Python (CPython), JavaScript (Node.js/V8), Java (JVM/JNI), Ruby (MRI), Go (plugins), and Rust (cdylibs) — six peer runtimes in one process.
$ omnivm run hello.py
hello from python
$ omnivm run App.java arg1 arg2
Hello! Args: arg1, arg2
$ omnivm run app.jar
server started on :8080
$ omnivm run main.go --flag value
args: [--flag value]
$ omnivm run main.rs
hello from rust
$ cat data.csv | omnivm run process.py
processed 1000 rows
$ omnivm run app.js --port 3000
listening on :3000# Build
docker build -t omnivm .
# Run scripts (language detected by extension)
docker run --rm omnivm run /omnivm/examples/hello.py
docker run --rm omnivm run /omnivm/examples/hello.js
docker run --rm omnivm run /omnivm/examples/hello.rb
# Run Java files (.java compiled in-memory, .class and .jar supported)
docker run --rm omnivm run /omnivm/examples/Hello.java
docker run --rm omnivm run /omnivm/examples/GsonDemo.java hello world
# Run Go programs (compiled as plugins, loaded in-process)
docker run --rm -v $(pwd)/main.go:/app/main.go omnivm run /app/main.go
# Run Rust programs (compiled as cdylibs, loaded in-process via dlopen)
docker run --rm -v $(pwd)/main.rs:/app/main.rs omnivm run /app/main.rs
# Pass arguments (all runtimes — goes to main(String[] args), sys.argv, etc.)
docker run --rm omnivm run /omnivm/examples/hello.py arg1 arg2
# Pipe stdin
echo "hello" | docker run -i --rm -v $(pwd)/upper.py:/app/upper.py omnivm run /app/upper.py
# Shebang support (inside container)
#!/usr/bin/env omnivm run
print("works as a script interpreter")
# Interactive REPL (all runtimes)
docker run -it --rm omnivm
# Inline execution (legacy syntax, still supported)
docker run --rm omnivm -python "print('hello')"
docker run --rm omnivm -js "console.log('hello')"
docker run --rm omnivm -java 'System.out.println("hello");'
docker run --rm omnivm -ruby "puts 'hello'"
docker run --rm omnivm -go 'fmt.Println("hello")'
docker run --rm omnivm -rust 'println!("hello");'OmniVM can act as a drop-in Python interpreter. When the binary is symlinked as python3 (or invoked as omnivm python), it delegates to CPython's Py_BytesMain() — the same code path as the stock python3 binary. Everything works: -m, -c, script files, pip, interactive REPL, PYTHONSTARTUP, -u, -W, stdin piping.
The difference: import omnivm is always available, giving Python code zero-overhead access to Go and JavaScript runtimes.
# Use as Python interpreter
docker run --rm --entrypoint python3-omnivm omnivm -c "print('I am Python')"
# Or via subcommand
docker run --rm omnivm python -c "print('I am Python')"
# Full CPython CLI works
docker run --rm --entrypoint python3-omnivm omnivm -m site
docker run --rm --entrypoint python3-omnivm omnivm -c "import sys; print(sys.version)"python3-polyscript is the progressive-migration entrypoint for existing Python applications. It is a small wrapper around stock python3, so the process starts as real CPython rather than the Go-hosted OmniVM binary. That matters for Passenger and Gunicorn prefork modes: the Go runtime is not loaded in the master process.
The wrapper preserves normal CPython behavior for .py code, preloads the polyscript package, and automatically installs a .poly import hook. A Passenger or Django app can swap its Python command first, then convert individual modules or call sites to PolyScript over time.
docker run --rm --entrypoint python3-polyscript omnivm \
-c "import polyscript, sys; print(polyscript.is_enabled(), sys.version)"The hook compiles .poly files with POLYSCRIPT_COMPILER (default: polyc). The Docker image includes polyc from the in-repo polyscript/ compiler package. Under python3-polyscript, imported .poly modules run the generated manifest in-process through CPython-hosted libomnivm by default; setting POLYSCRIPT_MANIFEST_RUNNER explicitly switches back to an external manifest runner. Existing Python imports keep using CPython; only .poly files enter PolyScript. This keeps python3-polyscript suitable for Passenger/Gunicorn: the master remains ordinary CPython and each worker loads libomnivm.so lazily after it has forked.
export POLYSCRIPT_COMPILER="polyc"
export POLYSCRIPT_CACHE_DIR="/tmp/polyscript-cache"
python3-polyscript manage.py runserverFor prefork servers, keep the master process clean and initialize runtime-heavy work in each worker after fork. Passenger can use python3-polyscript as the Python interpreter while passenger_wsgi.py remains ordinary Python during the first migration step. make test-libomnivm-stress covers this shape with master-import/worker-init, multi-worker, recycled-worker, and python3-polyscript WSGI smoke tests.
# passenger_wsgi.py
import os
os.environ.setdefault("POLYSCRIPT_CACHE_DIR", "/tmp/polyscript-cache")
from mysite.wsgi import applicationAs modules are converted, import billing_rules can resolve billing_rules.poly automatically, and retained manifest functions are exposed as Python callables. That means Django code can use normal imports such as from billing_rules import rank_user while the module body still executes through CPython-hosted libomnivm inside the worker.
python3-omnivm is still available for single-process tools and development. It is Go-hosted CPython and therefore loads the Go runtime before Python starts; use python3-polyscript for prefork deployments.
import omnivm
# Initialize only the runtimes you need (call in Gunicorn post_fork hook)
omnivm.init_runtimes(["go", "javascript"])
# Go — same thread, no IPC, no serialization, GIL released during execution
result = omnivm.call("go", "6 * 7") # "42"
# JavaScript — full Node.js with require() and npm packages
html = omnivm.call("javascript", "JSON.stringify({status: 'ok'})")
# Errors become Python exceptions, not process crashes
try:
omnivm.call("go", "invalid!!!")
except RuntimeError as e:
print(f"Caught: {e}") # Go compilation error, not a segfaultWrite Go as Go, not as strings. Compile plugins ahead of time and call exported functions directly:
# Load pre-compiled Go plugin
omnivm.load_plugin("go", "/app/plugins/sessvalidator.so")
# Call exported function — no compilation, no overhead
user_id = omnivm.call("go", 'sessvalidator.ValidateSession("session_key")')For Gunicorn/Passenger prefork servers, use libomnivm.so — a c-shared library loaded post-fork. The Go runtime starts fresh in each worker, avoiding the fatal "Go runtime doesn't survive fork()" problem.
FROM python:3.14-slim
# Install omnivm Python package (pure Python wrapper + libomnivm.so)
COPY --from=omnivm /usr/local/lib/libomnivm.so /usr/local/lib/
COPY --from=omnivm /usr/local/lib/python3.14/dist-packages/omnivm/ \
/usr/local/lib/python3.14/dist-packages/omnivm/
RUN ldconfig
# Build Go plugins as c-shared libraries (not -buildmode=plugin)
COPY go_plugins/ /tmp/go_plugins/
RUN cd /tmp/go_plugins/sessvalidator && \
go build -buildmode=c-shared -o /app/plugins/sessvalidator.so .
# Everything else is standard Django
COPY . /app
RUN pip install -r requirements.txt
CMD ["gunicorn", "myapp.wsgi:application", "--config", "gunicorn.conf.py"]# gunicorn.conf.py
preload_app = True # Django preloads in master — safe, no Go loaded yet
def post_fork(server, worker):
"""Each worker loads the Go runtime fresh after fork."""
import omnivm
omnivm.init_runtimes(["go", "javascript", "java", "ruby"]) # dlopen("libomnivm.so")
omnivm.set_task_timeout(5000) # watchdog for direct JS/Ruby calls
omnivm.load_plugin("go", "/app/plugins/sessvalidator.so")
omnivm.execute("javascript", "global.marked = require('marked')")
omnivm.execute("ruby", "require 'json'")
def worker_exit(server, worker):
"""Optional: release live OmniVM handles before an app-server worker exits."""
import omnivm
omnivm.drain_worker_hook(server, worker)
# middleware.py
from omnivm import call
class GoSessionMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
session_key = request.COOKIES.get("sessionid")
if session_key:
user_id = call("go", f'sessvalidator.ValidateSession({session_key!r})')
if user_id:
request._go_validated_user_id = user_id
return self.get_response(request)
# views.py
from omnivm import call
import json
def my_view(request):
# Go for CPU-bound work (GIL released — other threads aren't blocked)
hash_result = call("go", f'hasher.ComputeHash({request.path!r})')
# Node.js for npm ecosystem
html = call("javascript", f'marked.parse({json.dumps(markdown_text)})')
return JsonResponse({"hash": hash_result, "html": html})Why c-shared instead of the OmniVM binary? Go's runtime (GC, scheduler, goroutines) doesn't survive fork(). A Go binary is a running Go runtime from the moment it starts — forked children inherit corrupted state. libomnivm.so sidesteps this: the master process is pure CPython (no Go), and each worker dlopens the library post-fork, starting a fresh Go runtime.
Go plugins use -buildmode=c-shared, not -buildmode=plugin. Go's plugin system requires the host binary to be a regular executable with plugin metadata tables; a c-shared host can't load plugins. Instead, Go plugins are built as c-shared libraries themselves and loaded via dlopen/dlsym.
Direct-call watchdog semantics in c-shared mode are intentionally explicit:
| Runtime | Timeout behavior |
|---|---|
| JavaScript | omnivm.set_task_timeout(ms) can preempt direct calls and nested bridge calls with V8 termination. |
| Ruby | omnivm.set_task_timeout(ms) can preempt direct calls and nested bridge calls through Ruby's interrupt hook. |
| Python | Host CPython is interrupted with CPython-native mechanisms; direct libomnivm watchdog arming is not used for Python code. |
| Java | omnivm.set_task_timeout(ms) calls Thread.interrupt() on the active Java thread. This stops interruptible calls such as Thread.sleep() and blocking Java APIs; CPU-bound Java code must check interruption cooperatively. |
| Go plugins | omnivm.set_task_timeout(ms) applies a host-call deadline and returns control to CPython. Arbitrary in-process Go plugin code cannot be force-preempted; recycle the worker after a plugin deadline. |
You can inspect the current matrix and worker health at runtime:
import omnivm
omnivm.init_runtimes(["javascript", "java", "ruby"])
print(omnivm.watchdog_capabilities())
print(omnivm.status())
if omnivm.worker_tainted():
# Let your process manager recycle this worker after the request.
print(omnivm.worker_taint_reason())worker_tainted() is intentionally conservative. It is set after a Go plugin deadline because libomnivm can return control to CPython, but arbitrary in-process Go plugin code may still be running and cannot be safely force-preempted.
In c-shared mode there is no Go-owned background dispatcher thread. Direct calls cooperatively pump async runtimes on the pinned CPython worker thread, so Node/libuv timers such as setTimeout() advance on subsequent omnivm.call() / omnivm.execute() boundaries without violating CPython thread-state ownership.
A blocking .poly/manifest run holds the one OmniVM thread (the "Golden Thread") for its whole duration. Under an async or gevent worker that would freeze the event loop / hub and stall every other in-flight request in that worker. OmniVM can instead run a manifest cooperatively: the work is driven step-by-step and the host scheduler is given control between guest operations — while every guest eval still runs on the single Golden Thread, and no new OS threads are created.
import omnivm
# gevent worker: invisible — a normal synchronous call auto-cooperates when the
# gevent monkeypatch is active, so the hub keeps serving other greenlets.
result = omnivm.run_manifest("feature.poly.json")
# asyncio / ASGI worker: await it; the event loop stays responsive.
result = await omnivm.run_manifest_async("feature.poly.json")
# Opt out (raw, always blocking):
result = omnivm.run_manifest_blocking("feature.poly.json")Measured on a manifest that does ~1s of cross-runtime work: a concurrent background task gets 0 ticks during a blocking call (loop/hub frozen) versus 130–155 ticks during a cooperative call, with no slowdown. See make test-cooperative.
Cooperative goroutine join. A .poly that spawns a pure-Go goroutine (go work()) runs that goroutine in parallel on a real OS thread while the host stays responsive across the wait() join — true parallelism for Go-native work, plus a responsive worker.
Limitation: a manifest that drives OmniVM's internal asyncio (Python async/await) can't run under a running asyncio loop — Python forbids nesting asyncio.run() on one thread — so those fall back to blocking under run_manifest_async. gevent is unaffected. (Rust async is already safe: its bridge calls drain to the Golden Thread.)
A .poly Go function can invoke a callback owned by another language. The callback always runs on the Golden Thread, so it never breaks a non-thread-safe app's single-threaded assumptions:
# add_one is a JavaScript (or Python) function passed into a Go function:
# function add_one(x) { return x + 1 }
# func apply(cb, n) { return cb(n) } // Go invokes the guest callbackWhen the Go function runs on the Golden Thread the callback is invoked inline. When it runs inside a spawned goroutine, the callback invocation is auto-marshaled back to the Golden Thread and serviced by a time-free pumping-wait — so it works without deadlock and without any hardcoded timeout (the pumping-wait structurally prevents the cyclic deadlock rather than detecting it). Works in both the c-shared (libomnivm) and in-process (manifest-runner) plugin modes. See make test-go-callbacks.
The one rule worth knowing: a goroutine should produce Go-native work and let the Golden Thread consume it; a guest callback fired from a goroutine is marshaled (correct, slightly slower), while pure-Go goroutine work runs truly in parallel.
Thread-local call timing for Django middleware:
from omnivm import call, thread_local_total_ms, thread_local_reset
class OmniVMMetricsMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
thread_local_reset()
response = self.get_response(request)
response["X-OmniVM-Time-Ms"] = f"{thread_local_total_ms():.2f}"
return responseStructured cross-runtime errors for application logs:
import logging
import omnivm
logger = logging.getLogger(__name__)
try:
omnivm.call("javascript", "throw new Error('boom')")
except omnivm.RuntimeError as exc:
logger.exception(
"omnivm runtime error",
extra={"omnivm_error": exc.to_dict()},
)RuntimeError.to_dict() and its as_dict() alias return a JSON-serializable envelope with runtime,
origin_runtime, type, message, traceback, stack_frames, cause_chain,
boundary_path, original_error_handle, and details. The handle is only populated when
the source runtime reports one; callers should treat it as optional diagnostic
metadata. Structured cause entries preserve their own optional runtime,
origin_runtime, boundary_path, and original_error_handle fields when a
source envelope provides them. Native JavaScript Error objects created by OmniVM expose the same
envelope through toJSON(), so JSON.stringify(error) emits matching
snake_case fields. Python, JavaScript, and Ruby runtime error helpers keep
details, details_json, and detailsJson synchronized when code assigns a
new structured details object or a raw JSON details string.
| OmniVM as Python interpreter | libomnivm.so (c-shared) |
|
|---|---|---|
| How | Symlink python3 → omnivm |
import omnivm + init_runtimes() |
| Prefork (Gunicorn) | Not compatible (Go runtime dies on fork) | Works — Go loads post-fork |
| Single process | Works (gunicorn --workers 1 --threads N) |
Works |
pip install |
Not needed — it's the interpreter | pip install omnivm |
| All 5 runtimes | Yes (Python, JS, Go, Java, Ruby) | Yes (Python host + JS, Go, Java, Ruby) |
| Go plugins | -buildmode=plugin (standard) |
-buildmode=c-shared (dlopen) |
For most Django deployments (Gunicorn prefork), use the c-shared library.
| Function | Description |
|---|---|
omnivm.init_runtimes(["go", "javascript", "java", "ruby"]) |
Initialize specified runtimes after fork; the caller becomes the c-shared host thread |
omnivm.call(runtime, code) |
Eval expression, return result as string (GIL released) |
omnivm.execute(runtime, code) |
Execute code (for side effects) |
omnivm.load_plugin("go", path) |
Load pre-compiled Go plugin .so |
omnivm.set_task_timeout(ms) |
Set direct-call watchdog timeout for supported runtimes (0 disables) |
omnivm.watchdog_capabilities() |
Return the runtime timeout/preemption support matrix |
omnivm.host_thread_id() |
Return the OS thread id pinned by libomnivm |
omnivm.affinity_status() |
Return current Python thread/asyncio-loop affinity diagnostics relative to the libomnivm host thread |
omnivm.owner_dispatch_status() |
Return the machine-readable owner-dispatch/thread-affinity capability contract |
omnivm.owner_dispatch_target_status(target) |
Return the owner-dispatch capability for python_asyncio, javascript_event_loop, java_executor, or ruby_fiber_thread; common aliases such as asyncio, js, java, and ruby are accepted |
omnivm.assert_owner_dispatch_supported(label="") |
Raise a structured RuntimeError when an integration requires universal owner-loop/executor dispatch |
omnivm.assert_owner_dispatch_target_supported(target, label="") |
Raise a structured RuntimeError when an integration requires one specific owner-loop/executor target |
omnivm.ruby_threading_status() |
Return the embedded Ruby threading capability contract |
omnivm.assert_ruby_native_threads_supported(label="") |
Raise a structured RuntimeError when an integration requires native Ruby threads |
omnivm.assert_host_thread(label="") |
Raise a structured RuntimeError if a lifecycle callback is running off the libomnivm host thread |
omnivm.status() |
Return worker status JSON as a Python dict (pid, loaded runtimes, timeout counters, taint state, thread-affinity/Ruby threading boundaries, handle/boundary diagnostics) |
omnivm.drain_worker() |
Release live process handles and retained manifest modules before worker drain/reload hooks |
omnivm.drain_worker_hook(*args, **kwargs) |
App-server-compatible worker exit/reload hook that drains initialized workers and no-ops for workers that never loaded OmniVM |
omnivm.install_worker_drain_hook() |
Register drain_worker_hook() with atexit as an idempotent process-exit fallback |
omnivm.drain_finalizer_releases(max_releases=0) |
Best-effort request/job cleanup hook for queued proxy-finalizer releases (0 drains all queued releases) |
omnivm.lifecycle_scope(max_finalizer_releases=0) |
Sync/async context manager for request/job bodies that drains queued proxy-finalizer cleanup on exit without suppressing body errors |
omnivm.worker_tainted() |
Return whether this worker should be recycled after a non-recoverable timeout |
omnivm.worker_taint_reason() |
Return the recycle reason for diagnostics |
omnivm.last_timeout_runtime() |
Return the runtime that caused the last non-recoverable timeout |
omnivm.shutdown() |
Tear down runtimes (optional — process exit works too) |
omnivm.RuntimeError.to_dict() / as_dict() |
Return a structured runtime error envelope for logging, middleware, and JSON diagnostics |
omnivm.cleanup_errors(error) |
Return cleanup exceptions recorded while preserving a body exception |
omnivm.get_buffer(name) |
Return a borrowed shared buffer view, or None when the name is absent |
omnivm.set_buffer(name, data, dtype=0) |
Publish bytes-like data into the shared buffer store |
omnivm.release_buffer(name) |
Explicitly release a named shared buffer owner |
omnivm.buffer_owner(name[, data], dtype=0) |
Single-active-use sync/async context object for named buffer ownership; publishes optional data on entry, exposes owner.status(), and releases the owner name on exit |
omnivm.buffer_status(name) |
Return per-name buffer lifecycle diagnostics (state, lease_state, shape/stride/nullability metadata, and memory_space, currently host for zero-copy buffers) |
The named-buffer functions are explicit ownership tools for integrations that
publish or borrow shared host memory by name. Ordinary .poly snippets that
move buffers, tensors, Arrow arrays, ByteBuffers, memory views, or dataframe
columns across a runtime boundary should keep using native object access; they
should not need manual materialize/release helpers.
Guard failures such as assert_owner_dispatch_supported(),
assert_owner_dispatch_target_supported(), assert_host_thread(), and
assert_ruby_native_threads_supported() attach the relevant status block to
RuntimeError.details so startup code can log or branch on structured fields
without parsing the message.
Only the runtime you need is loaded. Running a Go file skips all embedded runtimes entirely (~60ms). Running a Python script only initializes CPython — no JVM, no Ruby, no V8 startup penalty.
OmniVM enhances errors with actionable suggestions:
$ omnivm run app.py
Traceback (most recent call last):
File "<string>", line 1, in <module>
ModuleNotFoundError: No module named 'requests'
Hint: pip install requests
$ omnivm run app.js
Error: Cannot find module 'express'
Hint: npm install express
$ omnivm run App.java
JavaError: Class not found: com.example.HttpClient
Hint: Ensure com.example.HttpClient is on the classpath.
Place JARs in ./lib/, ./libs/, or /omnivm/libs/
Maven: mvn dependency:copy-dependencies
Gradle: gradle copyDependencies
$ omnivm run main.go
./main.go:5:2: undefined: fmt.Prntln
Did you mean: fmt.Println?
Programs' exit codes propagate to the shell:
$ omnivm run exit42.py; echo $?
42All six runtimes are equal peers orchestrated by a Golden Thread dispatcher on the main OS thread. Cross-runtime calls happen synchronously on the same call stack. Go is the host only because its runtime was the pickiest about embedding — not because it has special status. Java files are compiled in-memory via javax.tools.JavaCompiler and executed on the embedded JVM — supporting .java, .class, and .jar files with auto-detected classpath. Go files are compiled as plugins (-buildmode=plugin), loaded in-process, and can call other runtimes via the bridge. Rust files are compiled as cdylibs against a pinned toolchain, loaded via dlopen under RTLD_LOCAL, and follow the same compiled-peer contract — one artifact works in both the binary and libomnivm.so deployments.
Go main goroutine (runtime.LockOSThread)
└─ Epoll dispatcher (Linux: eventfd + timerfd + libuv fd)
├─ Python (CPython 3.14) — GIL-wrapped entry, pipe-based interrupt
├─ JavaScript (Node.js 22 / V8) — v8::Locker, TerminateExecution
├─ Java (JVM 21 / JNI) — AttachCurrentThreadAsDaemon
├─ Ruby (MRI 3.3) — single VM thread, native Thread.new disabled
├─ Go (plugins) — compiled as .so, loaded via plugin.Open
└─ Rust (cdylibs) — dlopen + versioned C ABI, lazy current-thread tokio
C pthread watchdog (independent of Go scheduler)
└─ Temporal signal routing: active_runtime → per-runtime interrupt
Node.js is embedded via the C++ Embedder API with manual libuv pumping — uv_run(loop, UV_RUN_NOWAIT) gives JavaScript cooperative CPU time without starving other runtimes. This means require(), npm packages, setTimeout, Promises, and the full Node.js API all work.
On Linux, the dispatcher uses epoll with eventfd (task wakeup), timerfd (heartbeat), and the libuv backend fd (V8 I/O) — replacing the 1ms polling ticker with event-driven wakeups. A C pthread watchdog independently monitors task execution time and dispatches runtime-specific interrupts (Python pipe write, v8::Isolate::TerminateExecution(), Ruby trace hook interrupt, Java Thread.interrupt()).
When Python calls omnivm.call("javascript", "Math.sqrt(144)"):
- Python bridge (
py_omnivm_callinpkg/python/python.go): releases the GIL viaPyEval_SaveThread, calls the C function pointerg_bridge_call("javascript", "Math.sqrt(144)"). - Bridge gateway (
OmniCallin the main binary): receives the call on whatever thread invoked it. Looks up the target runtime and callsjsRuntime.Eval(code)directly — no dispatcher round-trip for bridge calls. - V8 entry (
omnivm_v8_evalinscripts/v8_bridge_node.cc): acquiresv8::Locker, enters the isolate/context, compiles and runs the code, returns the result as a C string. - Return path:
OmniCallreturns the result string. Python bridge re-acquires the GIL viaPyEval_RestoreThread, converts the C string to a Python object.
No thread ever holds two runtime locks simultaneously — the source lock is always released before acquiring the target lock. This makes deadlocks impossible by construction.
Main OS thread (Golden Thread):
runtime.LockOSThread() — pinned for lifetime of process
Runs: dispatcher loop, all scheduled tasks, V8/Python/Java direct calls
Ruby VM execution lane:
One Ruby execution lane is active inside OmniVM
Native Ruby Thread.new/Thread.start/Thread.fork are diagnostic-only
Puma-style in-process native thread ownership must run out of process
Rust execution lane:
Synchronous Rust runs ON the Golden Thread — a plain C call into the
dlopen'd cdylib, same OS thread, same call stack, zero new threads.
The tokio runtime is lazy and current-thread: it is never even created
until async Rust is used, and when it is, the Golden Thread itself
parks in tokio's reactor during awaits (no worker threads). Threads
appear only as explicit escalations: `go expr` on a sync fn (lazy
blocking pool), `executor = "multi"`, or crates that spawn internally
(rayon's pool, DNS resolvers). Pinned by a regression test
(TestSyncCallsStayOnGoldenThreadZeroTokio).
Foreign threads (JVM threads, Python threads, Go goroutines):
In binary bridge mode, can call runtimes through thread-safe bridge entry points
In c-shared Python-hosted mode, runtime entrypoints stay pinned to the host worker thread
Watchdog timeout protection only applies to Golden Thread tasks
The C pthread watchdog (pkg/watchdog/) runs independently of Go's scheduler using pthread_cond_timedwait with CLOCK_MONOTONIC. When armed:
- The dispatcher sets
active_runtimebefore each task - If the task exceeds the timeout, the watchdog fires the runtime-specific interrupt
- The interrupt fires once for the timed-out task; callers re-arm the watchdog for the next task
- A generation counter prevents stale timeouts after rapid arm/disarm cycles
Rust slots in next to Go as a compiled peer: source compiles to a cdylib
against the image's pinned toolchain, loads in-process via dlopen
(RTLD_LOCAL, versioned bridge ABI, SHA256 artifact cache), and calls other
runtimes through the same bridge. With a warm cache a call is a dlopen;
cold, it is a cargo build against a pre-compiled prelude (top crates baked
into the image: tokio, serde, reqwest with rustls, polars, rayon, sqlx, axum,
anyhow, chrono, itertools, regex, ...). Design doc:
docs/rust-runtime-design.md.
// hello.rs — omnivm run hello.rs
fn main() {
let big = omnivm::call("python", "2 ** 100").unwrap();
println!("python says {big}");
}Golden-thread-first. Rust is the only guest without a GIL/GVL — the easy mistake would be giving it background threads by default. It doesn't get any:
- Sync Rust = zero threads, zero tokio. A synchronous call runs on the Golden Thread as a plain C function call — same stack as every other runtime. The tokio runtime is never constructed for call-stack-only code.
- Async Rust = still zero threads. First async use lazily creates a
current-thread tokio runtime; a manifest
awaitis a re-park loop where the Golden Thread itself parks in tokio's reactor, with select arms for the heartbeat (pumping libuv/asyncio between parks), the dispatcher's task eventfd (so parked awaits never starve dispatcher work), and the async bridge hop (outbound calls exit the park before running — deep cross-runtime recursion stays sequential, never nested). - Threads are explicit escalations:
go expron a sync fn uses tokio's lazy blocking pool;executor = "multi"(envOMNIVM_RUST_EXECUTOR=multi) opts into a multi-thread runtime with completions delivered to the dispatcher via eventfd and arust_executorowner-dispatch target; rayon's pool initializes on firstpar_iter(the "GIL released, CPU-bound" escalation).
Boundary semantics. serde is the codec: any Serialize/Deserialize
type crosses by its author-declared shape — #[serde(tag = "type")] enums
arrive in JS as discriminated unions, in Python as tagged dicts.
Result<T, E> at the boundary becomes the structured error envelope (anyhow
context chains surface in err.message); panics are caught at every entry
point and become catchable RuntimeErrors, never dead workers (aborts taint
the worker — same recycle path as a Go plugin deadline). Exported fns take
owned, concrete params (&str/&[T] get mechanical owned-data adapters;
generic fns stay internal — call them through a concrete wrapper). Stateful
objects (reqwest::Client, pools, compiled regexes) persist across calls as
unit statics or as handle-table proxies.
Arrow-native tabular crossing. DataFrames cross via Arrow: pandas/pyarrow
→ polars arguments use the C Data Interface pointer handoff — zero-copy,
asserted by pointer identity in the test corpus (the buffer address Python
exports is the address the Rust DataFrame reads). Rust fns just declare
fn heavy_stats(frame: DataFrame) -> DataFrame — no adapter code.
PolyScript mixing. Rust mixes with the other runtimes in .poly files
with no annotations — fn, macros (format!(), .await, use paths, and
match arms are definite Rust evidence, and whole items (with lifetimes,
generics, where-clauses, attributes) are sliced verbatim by a Rust-aware item
scanner, so real existing Rust drops in unchanged:
import pandas as pd
use polars::prelude::*;
use rayon::prelude::*;
fn heavy_stats(frame: DataFrame) -> DataFrame {
frame.lazy().group_by([col("user_id")])
.agg([col("score").mean().alias("avg_score")]).collect().unwrap()
}
df = pd.read_parquet("/data/reviews.parquet") # python
const stats = heavy_stats(df) # rust, zero-copy Arrow
print(f"rows: {stats.height}") # python againGradual typing. Rust fns in .poly files may omit param and return
types (fn top_score(reviews) { ... }). Untyped params complete to
omnivm::Dyn — Python-flavored dynamic values (r["score"] indexing,
iteration, arithmetic/comparison, as_f64() accessors; type errors are
catchable Python-style TypeError/KeyError panics) — and an omitted
return completes to -> impl Serialize. When every call site agrees
(integer/float/string/bool literal, DataFrame provenance), the concrete
type is stamped instead and the typed/df lanes follow. Fully typed Rust is
never rewritten.
The acceptance example (polyscript/examples/rust-review-service.poly, a
four-language review service) and a nine-file ecosystem corpus
(tokio/reqwest, axum, sqlx, regex, chrono, anyhow/thiserror, itertools,
rayon, Arrow C-Data) run end-to-end in CI under both the binary and
libomnivm.so hosts (scripts/test-rust-corpus.sh, four-state ratchet), and
a round-trip oracle asserts real crate source files slice byte-identically
through the compiler. Prefork-safe (children initialize Rust post-fork;
covered by scripts/test-prefork-rust.py).
The bridge function omnivm.call(runtime, code) is available from every runtime.
In the OmniVM binary/REPL bridge it is thread-safe; in c-shared Python-hosted
deployments, direct runtime, manifest, plugin, and typed-call entrypoints are
pinned to the initialized CPython worker thread and expose
omnivm.owner_dispatch_status() / omnivm.assert_host_thread(label) for
startup and callback checks.
# Python calling JavaScript
result = omnivm.call("javascript", "Math.sqrt(144)")
# JavaScript calling Ruby
var result = omnivm.call("ruby", "('hello' + ' world').upcase");
# Java calling Python (JVM-spawned threads are supported in binary bridge mode)
String result = omnivm.OmniVM.call("python", "2 ** 100");
# Go calling Python (via plugin bridge)
result := OmniVM.Call("python", "2 ** 100")
# Rust calling Python (sync; from async use omnivm::call_async — it hops
# out of the tokio park so re-entry stays sequential)
let result = omnivm::call("python", "2 ** 100")?;All calls execute synchronously — no marshalling, no IPC, no serialization. Golden Thread calls are direct C function calls. Binary bridge foreign-thread calls acquire the target runtime's entry lock where that mode supports them; c-shared host calls fail fast from non-host threads instead of trying to migrate work onto an owner loop.
:python, :py Switch to Python
:javascript, :js Switch to JavaScript
:java, :jvm Switch to Java
:ruby, :rb Switch to Ruby
:go Switch to Go
:rust, :rs Switch to Rust
:status Show runtime status
:quit, :q Exit
An Express.js HTTP server where route handlers call Python, Ruby, and Java — all on the same thread:
docker run --rm --entrypoint express-demo omnivmStarting Express server...
Express listening on :3000
--- GET / ---
Status: 200 OK
Body: {"message":"Hello from Express inside OmniVM!",
"python":"3.14.3","ruby":"3.2.3","java":"21.0.10",
"engine":"Node.js v22.22.2"}
--- GET /compute ---
Status: 200 OK
Body: {"fibonacci_50":"12586269025","ruby_reverse":"MVinmO"}
The manifest executor runs structured JSON programs that dispatch ops across all five runtimes. A manifest is the IR target produced by the PolyScript compiler — each op specifies a runtime, code, captures, bindings, and control flow.
Manifest proxies are designed so generated .poly code can use normal field
reads, method calls, indexing, iteration, membership, length, and lifecycle
cleanup even when owner objects have fields named then, items, keys,
get, close, length, or count. Runtime-specific proxy helpers such as
omnivm.proxyGet, Python proxy_get, Ruby omnivm_get, and Java
OmniVM.proxyGet remain available for diagnostics and extremely rare manual
escape-hatch debugging, but .poly user code should not need them in normal
use. If it does, treat that as a bug in the automatic proxy/codegen behavior.
The same rule applies to native-ish values: generated .poly code should pass
and inspect buffers, tensors, Arrow arrays, Java ByteBuffers, Python
memoryviews, and similar host-memory objects through the manifest boundary
without rewriting snippets into get_buffer, to_buffer, to_arrow, or
manual release calls.
JavaScript handle and stream proxies also expose Symbol.dispose and
Symbol.asyncDispose when available for generated cleanup paths. Embedded
JavaScript also provides
omnivm.bufferOwner(name[, data], dtype[, callback]) for scoped named-buffer
ownership with single-active-use entry and idempotent release. Owners expose status(), and
omnivm.bufferStatus(name) returns the same per-name lifecycle diagnostics.
JavaScript code that needs owner-loop routing can preflight the current
diagnostic-only boundary with omnivm.ownerDispatchStatus(),
omnivm.ownerDispatchTargetStatus(target),
omnivm.assertOwnerDispatchSupported(label), or
omnivm.assertOwnerDispatchTargetSupported(target, label).
JavaScript integrations that may load native-threaded Ruby hosts can
preflight that separate boundary with omnivm.rubyThreadingStatus() or
omnivm.assertRubyNativeThreadsSupported("ruby host startup").
Owner fields named close or dispose remain ordinary fields on retained
Python proxies; generated cleanup uses the proxy lifecycle path instead of
treating those names as lifecycle methods. Python retained handle proxies, stream
iterators, and embedded local stream proxies also support with and
async with for deterministic release/cancel, and Python stream proxies support
async for with the same early-break cancellation as sync iteration. For
ordinary local objects, generated lifecycle helpers honor descriptor-defined
close() and dispose() without triggering dynamic attribute lookup and await
async close, aclose(), and dispose results when needed.
Ruby manifest proxies likewise keep owner fields such as then, class,
inspect, hash, to_s, to_h, to_a, to_json, close, and dispose
as ordinary fields when present. Embedded Ruby also
provides OmniVM.buffer_owner(name[, data], dtype: 0) for scoped named-buffer
ownership with single-active-use entry and idempotent release. Owners expose
status(), and OmniVM.buffer_status(name) returns the same per-name lifecycle
diagnostics.
Ruby code that may load native-threaded app servers can call
OmniVM.ruby_threading_status or
OmniVM.assert_ruby_native_threads_supported("puma startup") before invoking
framework startup. Ruby code that needs owner-loop routing can also preflight
the diagnostic-only boundary with OmniVM.owner_dispatch_status,
OmniVM.owner_dispatch_target_status(target),
OmniVM.assert_owner_dispatch_supported(label), or
OmniVM.assert_owner_dispatch_target_supported(target, label).
Generated Java manifest code also treats proxy collisions as automatic owner
shape decisions rather than a reason for user .poly code to call static proxy
helpers. Java also provides OmniVM.bufferOwner(name[, data], dtype) as an AutoCloseable
single-active-use named-buffer owner for try-with-resources cleanup.
OmniVM.bufferStatus(name) and owner.status() return JSON lifecycle
diagnostics from the shared buffer store.
Java code that needs owner-executor routing can preflight the current
diagnostic-only boundary with OmniVM.ownerDispatchStatus(),
OmniVM.ownerDispatchTargetStatus(target),
OmniVM.assertOwnerDispatchSupported(label), or
OmniVM.assertOwnerDispatchTargetSupported(target, label).
Java integrations that may load native-threaded Ruby app servers can preflight
that separate boundary with OmniVM.rubyThreadingStatus() or
OmniVM.assertRubyNativeThreadsSupported("puma startup").
Go manifest callers can use manifest.ProxyClose(value) or the idiomatic alias
manifest.OmniVMClose(value) to close handle proxies, stream proxies, or
ordinary Go Close() error values through the same collision-safe lifecycle
path; the compatibility alias manifest.OmnivmClose(value) remains available
for generated-helper naming parity. The helpers return (closed, error) so
failed owner cleanup is not mistaken for a successful close.
Go integrations that need owner-loop/executor routing can preflight the same
diagnostic-only boundary with manifest.OwnerDispatchStatus(),
manifest.OwnerDispatchTargetStatus(target),
manifest.AssertOwnerDispatchSupported(label), or
manifest.AssertOwnerDispatchTargetSupported(target, label). Go code that may
load native-threaded Ruby app servers can also call
manifest.RubyThreadingStatus() or
manifest.AssertRubyNativeThreadsSupported(label).
# Run a single manifest
docker run --rm --entrypoint manifest-runner omnivm /omnivm/examples/cursed-concurrency.json
# Verify boundary decisions without starting guest runtimes
docker run --rm --entrypoint manifest-runner omnivm --doctor /omnivm/examples/cursed-concurrency.json
# Run the focused spawn/channel contract regression
docker run --rm --entrypoint manifest-runner omnivm /omnivm/examples/spawn-channel-contract.json
# Showcase examples
docker run --rm --entrypoint manifest-runner omnivm /omnivm/examples/fizzbuzz-polyglot-manifest.json
docker run --rm --entrypoint manifest-runner omnivm /omnivm/examples/data-pipeline-manifest.json
docker run --rm --entrypoint manifest-runner omnivm /omnivm/examples/polyglot-pipeline-manifest.json
# Run the full manifest test suite
make test-manifests| Op | Description |
|---|---|
exec |
Execute code (side effects, stdout capture) |
eval |
Evaluate expression (returns value) |
import |
Runtime-specific module import |
func_def |
Define a manifest function (with optional generator, Go plugin source) |
return |
Return from function |
if |
Conditional branching with arms + else |
loop |
While/for/foreach/infinite loops |
declare / assign |
Variable binding and mutation |
concat |
String interpolation with cross-runtime eval segments |
try / throw |
Error handling with catch/finally |
parallel |
Concurrent branch execution |
chan |
Go channel operations (make/send/recv/close) |
select |
Go-style select on channels |
spawn |
Launch Go functions or manifest func_defs, optionally binding a spawn handle |
resource |
Open/close opaque runtime-owned resources with explicit disposer metadata |
table |
Export/release runtime-owned table or buffer handles, preferably Arrow C Data Interface |
job |
Enqueue, complete, and wait on manifest-visible background job handles |
yield |
Generator yield (with delegate support) |
await |
Async/await semantics |
Channel, spawn, and wait() semantics are defined in
docs/manifest-channel-contract.md.
Cross-runtime value movement is specified in
docs/boundary-semantics.md, with the staged
performance plan in
docs/bridge-performance-plan.md.
Manifests are validated when parsed. The validator checks the stable executor contract: supported runtimes and op names, required fields for ops such as spawn, chan, select, func_def, and nested control-flow bodies. It intentionally does not reject dynamic binding-liveness cases that only execution can know.
Manifest channels are shared executor values, not runtime-local queues. chan ops create them, send/recv/close ops mutate them, and captures inject iterable/readable wrappers into runtimes such as JavaScript and Python. The Go manifest helpers recv(ch) and send(ch, value) expose the same channel values to compiled Go worker functions.
spawn returns a manifest-visible handle when the op has a bind field:
{ "op": "spawn", "runtime": "go", "code": "worker(1)", "bind": "w1" }The wait helper has three forms:
| Form | Result |
|---|---|
wait() |
Waits for every spawned worker and returns the total spawn count |
wait(handle) |
Waits for one handle and returns that worker's result |
wait(h1, h2, ...) |
Waits in argument order and returns an array of worker results |
This is what lets a .poly source file express real worker joins:
const w1 = go worker(1)
const w2 = go worker(2)
const joined = wait(w1, w2)
The spawn-channel-contract.json example is the small regression manifest for this behavior. cursed-concurrency.json is the larger end-to-end example that combines Go workers, shared channels, JavaScript channel iteration, and Python aggregation.
The stress suite verifies correctness under pressure:
docker run --rm --entrypoint stresstest omnivmTests cover cross-runtime stack mixing, generators across C boundaries, asyncio pumping with bridge callbacks, re-entrant calls (Python → JS → Python), signal handling (JVM SIGSEGV + Ruby + Python interrupts), GC interaction, 1MB string round-trips, Ruby Fiber cooperative bridging, 4-runtime mutual recursion (18 levels deep), Golden Thread verification, pthread_atfork fork guard, watchdog-driven preemption of infinite loops across all runtimes, binary-mode foreign-thread bridge calls (JVM threads → Python/JS/Ruby), c-shared host-thread affinity diagnostics, concurrent multi-thread bridge contention, and nested foreign-thread cross-runtime chains.
The pkg/omnivm package lets you embed OmniVM as a Go library — no CLI required. This is designed for production use cases like a Go HTTP server calling Django's ORM:
package main
import (
"context"
"fmt"
"log"
"net/http"
"os/signal"
"runtime"
"syscall"
"time"
"github.com/omnivm/omnivm/pkg/omnivm"
"github.com/omnivm/omnivm/pkg/python"
)
func init() { runtime.LockOSThread() }
func main() {
vm := omnivm.New(omnivm.Config{
TaskTimeout: 30 * time.Second,
DrainTimeout: 25 * time.Second,
})
// Only load what you need — no JVM, no Ruby, no V8 overhead
vm.Register("python", python.New())
if err := vm.Start(); err != nil {
log.Fatal(err)
}
// Django setup — runs once, state persists across all calls
vm.Execute("python", `
import os, django
os.environ['DJANGO_SETTINGS_MODULE'] = 'myapp.settings'
django.setup()
`)
// DB cleanup after every call (runs even on error, like defer)
vm.SetAfterCall("python",
"from django.db import close_old_connections; close_old_connections()")
ctx, cancel := signal.NotifyContext(context.Background(),
syscall.SIGTERM, syscall.SIGINT)
defer cancel()
go func() {
http.HandleFunc("/api/user", func(w http.ResponseWriter, r *http.Request) {
// Per-request context — cancels if client disconnects
result, err := vm.CallWithContext(r.Context(), "python", fmt.Sprintf(
`from apps.models import User; User.objects.get(id=%%q).to_json()`,
r.URL.Query().Get("id"),
))
if err != nil {
http.Error(w, "internal error", 500)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(result))
})
log.Fatal(http.ListenAndServe(":8080", nil))
}()
vm.Run(ctx) // blocks on Golden Thread
vm.Shutdown() // drain hooks → runtime teardown (reverse order)
}| Method | Description |
|---|---|
New(Config) |
Create a VM instance |
Register(name, runtime) |
Add a runtime (selective — only load what you need) |
Start() |
Initialize runtimes on Golden Thread |
Run(ctx) |
Block running the dispatcher (returns on context cancel) |
Call(runtime, code) |
Eval code, return result as string (goroutine-safe) |
CallWithContext(ctx, runtime, code) |
Call with per-request deadline/cancellation |
CallWithRequestID(ctx, runtime, code, id) |
Call with request ID for metrics correlation |
CallFast(runtime, code) |
Priority eval — skips ahead of queued normal calls |
CallFastWithContext(ctx, runtime, code) |
Priority eval with deadline/cancellation |
CallFastWithRequestID(ctx, runtime, code, id) |
Priority eval with request ID |
Execute(runtime, code) |
Run code, return captured stdout (goroutine-safe) |
ExecuteWithContext(ctx, runtime, code) |
Execute with per-request deadline/cancellation |
LoadFile(runtime, path) |
Execute a file's contents (define helpers from .py files) |
SetAfterCall(runtime, code) |
Cleanup code that runs after every call (like defer) |
SetOnCallDone(fn) |
Observe-only callback with CallMetrics (duration, queue wait, fast/normal, request ID) |
CallBatch(runtime, items) |
Execute multiple independent snippets in one Golden Thread dispatch |
CallBatchWithContext(ctx, runtime, items, requestID) |
Batch call with context and request ID |
RegisterDrainHook(fn) |
Shutdown hook — runs on Golden Thread, can call drainExecute() |
Shutdown() |
Graceful stop: drain hooks (on Golden Thread) → reverse-order runtime teardown |
The interpreter is persistent — variables and functions survive across calls. The recommended pattern is to define Python helper functions at startup (from files), then call them with one-liners per request:
# helpers/user.py
import json
from django.contrib.auth.models import User
def get_user_json(user_id):
u = User.objects.get(id=int(user_id))
return json.dumps({"email": u.email, "active": u.is_active})
def validate_session(session_key):
from django.contrib.sessions.backends.db import SessionStore
s = SessionStore(session_key=session_key)
return s.get("_auth_user_id", "")// At startup — load helpers from files (not inline strings)
vm.LoadFile("python", "helpers/user.py")
// Per request — clean one-liner
result, err := vm.Call("python", fmt.Sprintf(`get_user_json(%q)`, userID))
sessionUID, err := vm.Call("python", fmt.Sprintf(`validate_session(%q)`, sessionKey))The string-returning Call API is the compatibility surface, so helpers that
produce HTTP JSON can still return JSON directly. That is not the long-term
boundary model for PolyScript or manifest execution: framework objects, ORM
models, buffers, streams, and other complex values should cross automatically
through the generic copy, ref, stream, or Arrow boundary selected from the
value's protocol shape. The runtime must not rely on special cases for Django,
Pandas, PIL, or any other package name.
The dispatcher has two channels: a fast channel (64 slots) and a normal channel (256 slots). Fast tasks are always drained before normal tasks on every dispatcher cycle, reducing head-of-line blocking.
Use CallFast for latency-sensitive operations (auth checks, session validation) and Call for heavier business logic (report generation, batch processing). A slow Python query in the normal queue won't block fast auth checks queued behind it:
// Auth middleware — uses priority channel, skips ahead of slow queries
userID, err := vm.CallFast("python", fmt.Sprintf(`validate_session(%q)`, sessionKey))
// Business logic — normal priority
report, err := vm.Call("python", fmt.Sprintf(`generate_report(%q)`, reportID))When a single HTTP handler needs multiple independent pieces of data, CallBatch executes them all in one Golden Thread dispatch — avoiding N round-trips through the dispatcher queue:
results := vm.CallBatch("python", []omnivm.BatchItem{
{Code: "get_subscription_state(123)"},
{Code: "get_usage_totals(123)"},
{Code: "get_lock_status(123)"},
})
// results[0].Value, results[0].Err — independent per item
// results[1].Value, results[1].Err
// results[2].Value, results[2].ErrEach item gets independent error handling — a failure in item 1 does not prevent item 2 from executing. AfterCall runs once after all items complete (not per-item). Use CallBatchWithContext for context cancellation and request ID correlation.
SetOnCallDone receives a CallMetrics struct with production-grade telemetry:
vm.SetOnCallDone(func(m omnivm.CallMetrics) {
histogram.WithLabelValues(m.Runtime, fmt.Sprint(m.Fast)).
Observe(m.Duration.Seconds())
if m.QueueWait > 50*time.Millisecond {
log.Warn("high dispatcher queue wait",
"request_id", m.RequestID,
"queue_wait", m.QueueWait,
"exec_duration", m.Duration)
}
})| Field | Type | Description |
|---|---|---|
Runtime |
string |
Which runtime was called ("python", "javascript", etc.) |
Result |
string |
String result (empty on error) |
Err |
error |
nil on success |
Duration |
time.Duration |
Wall-clock execution time on the Golden Thread |
QueueWait |
time.Duration |
Time spent waiting in the dispatcher queue |
Fast |
bool |
true if dispatched via the high-priority channel |
RequestID |
string |
Caller-provided correlation ID (via CallWithRequestID / CallFastWithRequestID) |
All runtime calls serialize through the Golden Thread — Python and JavaScript cannot overlap. This is inherent to cgo and the GIL/GVL. The performance model is: Go handles HTTP routing and concurrency (fast), Python/JS/Ruby handle business logic (serialized but short). CallWithContext provides caller-side cancellation, but the Golden Thread task runs to completion (cgo cannot be interrupted mid-call).
SetAfterCall cleanup code (e.g., close_old_connections()) uses the lightweight Eval path internally, skipping the stdout/stderr capture overhead that Execute requires. This saves ~100μs per request compared to routing cleanup through Execute.
cmd/
omnivm/ Main binary (REPL + CLI + `run` subcommand + Python interpreter mode)
libomnivm/ c-shared library for pip-installable Python package (prefork-safe)
manifest-runner/ JSON manifest executor
stresstest/ 71-test stress suite
express-demo/ Express + Python/Ruby/Java HTTP demo
telephone/ Cross-runtime telephone game
pkg/
engine/ Shared runtime management core (used by both omnivm and libomnivm)
omnivm/ Library API (VM, Config, Call, Execute, Shutdown)
cli/ CLI parsing, language detection, shebang handling
errmsg/ Error enhancement (hints, traceback formatting)
golang/ Go runtime (plugin-based, in-process compilation + execution)
rust/ Rust runtime (cdylib compile/cache/dlopen, bridge ABI, tokio drive loop)
python/ CPython embedding via cgo
javascript/ Node.js/V8 embedding via cgo
jvm/ JVM embedding via JNI/cgo
ruby/ MRI Ruby embedding via cgo
manifest/ Manifest IR executor (ops, captures, channels, stubs)
dispatcher/ Golden Thread task serializer (epoll on Linux)
watchdog/ C pthread watchdog with temporal signal routing
signals/ Signal handler management
arrow/ Shared memory primitives
scripts/
v8_bridge_node.cc Node.js ↔ v8_bridge.h C++ adapter
test-manifests.sh Manifest test suite runner
test-cli.sh CLI integration tests (29 tests)
test-libomnivm-*.sh CPython-hosted libomnivm manifest/stress tests
test-poly-libomnivm-smoke.sh Compile PolyScript examples and run via CPython + libomnivm
test-rust-corpus.sh Rust ecosystem corpus (four-state classification, ratcheted in CI)
test-prefork-rust.py Prefork fork-safety check for the Rust runtime
polyscript/ PolyScript lexer/parser/compiler, examples, and compiler tests
runtime/
java/ OmniVMRunner.java (in-memory compilation, file/jar/class execution)
rust/ Cargo workspace: omnivm support dylib (bridge/serde/tokio/Arrow), prelude warm-build
examples/ Manifest JSON files and sample scripts
docs/ Manifest contracts and design notes
Requires Docker. The multi-stage Dockerfile handles all dependencies (Go, CPython, Node.js, JVM, Ruby). Unit tests run during the build. Integration tests run against the final image.
# Build (runs unit + integration tests during build)
docker build -t omnivm .
# Run ALL tests against the built image
docker run --rm --entrypoint /bin/bash omnivm /omnivm/scripts/test-cli.sh # 29 CLI tests
docker run --rm --entrypoint stresstest omnivm # 71 stress tests
docker run --rm --entrypoint manifest-runner omnivm /omnivm/examples/manifest-test.json
# REPL
docker run -it --rm omnivmThe Docker build runs three test tiers: (1) unit tests with race detector for pure Go packages, (2) cgo-linked runtime tests for Python/JS/Ruby/Engine, and (3) cross-runtime integration tests that verify all runtimes initialize and interoperate correctly. CLI, stress, and manifest tests run against the final image.
Make targets:
make build # Build Docker image
make test-all # Canonical local/CI gate: local, Docker, manifest, stress, and libomnivm tests
make test-cli # CLI integration tests (29 tests in Docker)
make test-manifests # Run manifest examples and edge contract fixtures
make test-libomnivm-manifests # Run all example JSON manifests via CPython + libomnivm
make test-libomnivm-stress # Run CPython-hosted libomnivm stress checks
make test-libomnivm-stress STRESS_ARGS="--category proxy --name materializes" # Filter stress checks
make test-polyscript # Run PolyScript compiler tests, build, and manifest audit
make test-poly-libomnivm-smoke # Compile selected PolyScript examples, then run via CPython + libomnivm
make test-stress # Run 71 stress testsThe .poly smoke uses the in-repo PolyScript compiler package at polyscript/ by default. Prefer POLYSCRIPT_DIR for explicit alternate paths; the older GARBAGE_DIR variable remains accepted for existing scripts:
POLYSCRIPT_DIR=/path/to/polyscript make test-poly-libomnivm-smokeThe README-level CI parity sequence is:
# PolyScript compiler package
make test-polyscript
# omnivm
make test-all
make test-poly-libomnivm-smoke
make test-libomnivm-manifests
make test-libomnivm-stress- Lazy runtime initialization: Only the runtime needed for the target file is started.
omnivm run main.goskips all embedded runtimes.omnivm run script.pyonly starts CPython. - Java file execution:
omnivm run App.javacompiles in-memory viajavax.tools.JavaCompilerand runs on the embedded JVM with realmain(String[] args)and direct stdout/stderr. Supports.classand.jarfiles. Classpath auto-detects Maven (target/dependency/), Gradle (build/libs/), andlib//libs/directories — downloaded JARs just work. - Go as equal peer: Go files are compiled as plugins (
-buildmode=plugin), loaded in-process, and executed — not via subprocess.func main()is transformed to an exportedfunc Main()via the Go AST, compiled, and called viaplugin.Open/Lookup. Go plugins can call other runtimes through the bridge (OmniVM.Call("python", "...")) and participate in the REPL and inline execution (omnivm -go 'code'). Go is the host because its runtime was the pickiest about embedding, not because it has special status. - Rust as compiled peer, golden-thread-first: Rust always compiles to a cdylib (one artifact for binary and c-shared modes — no plugin/c-shared split), loaded via
dlopenwith a versioned bridge ABI (omnivm_set_bridge_v1; the ABI revision is baked into artifacts and into the cache key, so stale artifacts are refused with a structured error, never loaded silently). One tokio runtime per process lives in a support dylib all units link dynamically; everything Rust builds inside one cargo workspace with one lockfile (cargo derives symbol hashes from workspace, package selection, and flags — the build invariants are documented inpkg/rust/toolchain.go). Sync Rust adds zero threads and never initializes tokio; async parks the Golden Thread in the reactor; threads are explicit escalations only. - Bridge gateway affinity: The OmniVM binary bridge supports foreign-thread calls by entering the target runtime through the appropriate lock:
PyGILState_Ensure(Python),v8::Locker(V8), the Ruby VM execution lane, orAttachCurrentThreadAsDaemon(JVM). In c-shared Python-hosted mode, CPython owns the worker thread state, so runtime, manifest, plugin, and typed-call entrypoints reject non-host threads with structuredthread_affinitydiagnostics instead of attempting universal owner-loop dispatch. - Ruby single-VM-thread boundary: Ruby runs through one OmniVM-owned VM execution lane. Ruby 3.3's M:N threading breaks
Thread.newandrb_thread_call_without_gvlin this embedded shape, so native Ruby thread creation raises an explicit diagnostic instead of hanging. Rubyfork,Kernel.fork,Process.fork,Process.daemon,Process.spawn,Kernel.spawn,Kernel.system,Kernel.exec, backticks, andIO.popenalso raise explicit diagnostics after OmniVM initializes; Ruby code that needs preforking, daemonization, or subprocess launch must do so before loading OmniVM or run that component out of process.omnivm.status()["ruby_threading"]andomnivm.ruby_threading_status()report this boundary (mode=single_vm_thread, native threads unsupported), andomnivm.assert_ruby_native_threads_supported(label)is the fail-fast guard for host apps that need an out-of-process Puma deployment before loading a threaded Ruby app server. - Epoll dispatcher (Linux): eventfd for task wakeup, timerfd for heartbeat, libuv backend fd for V8 I/O. Replaces the 1ms polling ticker with event-driven wakeups — zero CPU when idle.
- C pthread watchdog: Independent of the Go scheduler.
pthread_cond_timedwaitwithCLOCK_MONOTONIC(immune to NTP jumps). Temporal signal routing dispatches runtime-specific interrupts: Python pipe write,v8::Isolate::TerminateExecution(), Ruby trace hook interrupt, and JavaThread.interrupt(). - Error enhancement: Missing module errors get "pip install" / "npm install" / "gem install" hints. Python tracebacks are reformatted with
file:linereferences. Go compile errors get "Did you mean?" suggestions. - Node.js over Duktape: Duktape was ES5.1 — no
const/let, no arrow functions, norequire(), no npm. Node.js (vialibnode-dev/libnode127) gives full ES2024+, the npm ecosystem, and built-in modules. - Skip
Py_FinalizeEx,ruby_cleanup(),V8::Dispose(): All crash in a polyglot process. Process exit reclaims resources. LD_PRELOAD=libjsig.so: JVM uses SIGSEGV for NullPointerException safepoints. Without signal chaining, this crashes Ruby. libjsig.so chains handlers properly.pthread_atforkfork guard: Child processes afterfork()have dead JVM threads holding mutexes. The guard_exit(71)s with a diagnostic stack trace — both the C backtrace (via glibcbacktrace_symbols_fd) and the Python traceback (viafaulthandler.dump_traceback) are logged to stderr, identifying exactly which dependency triggered the fork. Python forced tomultiprocessing.set_start_method('spawn'). The fork guard is conditional — it only fires when JVM or Ruby are loaded. Go+JS-only configurations are fork-safe when runtimes are initialized post-fork (the Gunicorn/Passenger pattern).- Python interpreter mode: When symlinked as
python3, OmniVM callsPy_BytesMain()— CPython's own entry point.PyImport_AppendInittab("omnivm", ...)registers theomnivmmodule before CPython initializes, soimport omnivmworks in any Python code. Best for single-process deployments (dev,gunicorn --workers 1 --threads N, uvicorn). Not compatible with prefork — Go's runtime doesn't survivefork(). - c-shared library mode (
libomnivm.so): For prefork servers (Gunicorn, Passenger, uWSGI). Built withgo build -buildmode=c-shared. All 5 runtimes are supported: JavaScript, Java, Ruby, Go (via dlopen plugins), and Python (host - cross-runtime bridge calls back into the already-running CPython). The master process is pure CPython - no Go runtime loaded. Each worker callsomnivm.init_runtimes()post-fork, whichdlopenslibomnivm.so, starts a fresh Go runtime, and pins the calling Python worker thread as the c-shared host thread. Direct calls and manifest execution must enter from that host thread; non-host callers fail fast with structuredthread_affinitydiagnostics. The background epoll dispatcher is intentionally not started in c-shared mode because CPython owns the process and thread state. Async runtimes are pumped cooperatively at host call boundaries, so Node/libuv timers progress without a Go-owned dispatcher thread. The watchdog, buffer bridge, cross-runtime bridge, and fork guard are active. Direct-call watchdog support is runtime-specific: JavaScript and Ruby can be preempted, Java receivesThread.interrupt(), Go plugin calls get a host-call deadline, and host Python uses CPython-native interruption. Workers exposeomnivm.status(),omnivm.owner_dispatch_status(), and conservative taint flags so servers can recycle after a non-recoverable Go plugin deadline and can fail fast when they require universal owner-loop/executor dispatch. PolyScript examples are compiled and executed through this path bymake test-poly-libomnivm-smoke; all example JSON manifests are covered bymake test-libomnivm-manifests, and CPython-hosted nested callback/buffer/fork/prefork lifecycle/watchdog checks are covered bymake test-libomnivm-stress. Seedocs/passenger-django-polyscript.mdfor the Passenger/Django migration shape anddocs/example-suite.mdfor example-suite coverage. Both binaries share thepkg/enginepackage for runtime lifecycle, bridge wiring, watchdog setup, and shutdown - the//exportC wrappers are thin. Go plugins must be built as-buildmode=c-shared(not-buildmode=plugin) and are loaded viadlopen/dlsym.