1
0
Fork 0
forked from fun/fun

More documentation about internals. No code changes (0.38.0)

This commit is contained in:
Johannes Findeisen 2026-01-28 16:55:39 +01:00
commit 058631e11b

View file

@ -256,8 +256,6 @@ From vm.h defaults (tuned for simplicity; adjust if needed):
## Concurrency, isolates, and garbage collection ## Concurrency, isolates, and garbage collection
### Short answer
We chose isolated state (like Lua) rather than a single global lock (like Pythons GIL). Each VM has its own heap, scheduler, and GC. There are no crossVM pointers. Concurrency and data exchange happen via message passing and a few carefully scoped sharedmemory primitives for highthroughput use cases. This keeps the C API simple, predictable, and safe to embed in multithreaded hosts. We chose isolated state (like Lua) rather than a single global lock (like Pythons GIL). Each VM has its own heap, scheduler, and GC. There are no crossVM pointers. Concurrency and data exchange happen via message passing and a few carefully scoped sharedmemory primitives for highthroughput use cases. This keeps the C API simple, predictable, and safe to embed in multithreaded hosts.
### Concurrency model ### Concurrency model
@ -296,6 +294,116 @@ Message passing and serialization are the defaults. When copying becomes too exp
This provides zerocopy handoff without coupling VMs or their collectors. This provides zerocopy handoff without coupling VMs or their collectors.
### Zero-Copy Shared Buffering
- We default to isolates for safety and scaling.
- Zerocopy sharing is done with `fun_shared_buffer`, an offheap, GCuntracked, pointerfree block thats immutable from the VMs point of view.
- Lifetime is managed with plain reference counting (`retain/release`).
- For hot paths, we also support an adoption (ownershiptransfer) pattern during message passing so the sender can drop its ref without copying.
#### How zerocopy is kept safe
- Offheap + untraced: `fun_shared_buffer` lives outside any VM heap, so perVM GCs never scan it and never need crossVM barriers.
- Immutable inside VMs: once a buffer is visible to any VM, it is treated as readonly by that VM code. That avoids aliasing hazards and lets multiple VMs parse the same bytes concurrently.
- Pointerfree contract: the buffer contains raw bytes only (no pointers into VM heaps), which prevents accidental crossheap reachability.
#### Lifetime model
- Global refcount on the shared object.
- `fun_shared_buffer_retain`/`fun_shared_buffer_release` drive deallocation.
- VMs just hold handles; the buffers memory is not traced by any VM.
- Host mutation policy:
- You may fill/mutate the buffer before publication (while only the host holds a ref).
- After passing it into any VM or port, treat it as immutable; further mutation must be done by creating a new buffer or by using hostside synchronization and not exposing the mutated view to VMs that assume immutability.
#### Ownership transfer (adoption)
- Message passing can “adopt” a buffer: instead of copying, the sender passes the handle and typically `release`s its ref after send.
- The receiver `retain`s upon receipt if it needs to outlive the message scope.
- This gives you zerocopy with a clear singlewriter → multireader handoff pattern.
#### Why not share GCmanaged objects?
- Sharing traced objects would require global safepoints or a fully barriered concurrent GC across VMs, or switching to atomic RC for those objects. We avoid that entirely; only offheap `fun_shared_buffer` is shared.
#### Minimal API (illustrative)
```c
// Zero-copy shared buffers (immutable inside VMs)
fun_shared_buffer_t* fun_shared_buffer_new(size_t n);
void* fun_shared_buffer_data(fun_shared_buffer_t*);
void fun_shared_buffer_retain(fun_shared_buffer_t*);
void fun_shared_buffer_release(fun_shared_buffer_t*);
// Ports/channels for inter-VM comms
fun_port_t* fun_port_create(fun_vm_t*);
int fun_send(fun_port_t*, fun_value_t value); // can carry a shared buffer handle
int fun_recv(fun_port_t*, fun_value_t* out, uint64_t timeout_ms);
```
#### Practical usage tips
- Default to isolates + ports for logic; use `fun_shared_buffer` only for large payloads (images, tensors, blobs).
- If you need shared mutability from native code, keep it outside the VM and guard with `fun_mutex_t`/`fun_rwlock_t` or `fun_atomic_*`. Dont expose mutable state to VMs.
- Document clearly when a buffer is “published” to VMs; after that point, treat it as immutable and rely on refcounts for lifetime.
#### Bottom line
- Its reference counting by default, with an optional ownershiptransfer handoff in message passing to avoid copies. This keeps the host API small and predictable while preserving GC isolation and safety.
### Immutable Buffer Sync via Ports
We dont expose shared mutability to VMs. The trick is: publishasimmutable plus adoption via ports. Ports/queues do the synchronization; `fun_shared_buffer` is offheap and refcounted with atomic ops. The host doesnt need to lock anything for the common paths.
#### What removes the hosts synchronization burden
- Immutable after publication
- Host fills a `fun_shared_buffer` while its private, then “publishes” it by sending through a port. From that point, all VM code treats it as readonly. No data races; no host locks.
- Port/queue owns the concurrency
- `fun_send`/`fun_recv` operate on an internal MPMC queue with the necessary atomics/fences. You dont implement a queue nor protect it; the runtime does.
- Adoption (ownership transfer)
- `fun_send` can adopt a buffer handle. Sender drops its ref after send; receiver retains if needed. Lifetime is clear without locks.
- Atomic refcounting only
- The only shared mutable state is the buffers global refcount, updated atomically inside the runtime. You dont touch it.
- VM thread affinity
- You never call into a VM from arbitrary threads; use `fun_vm_post` to hop to the owning thread. This avoids hostside marshaling races.
#### Typical pattern (no locks needed)
```c
fun_shared_buffer_t* b = fun_shared_buffer_new(n);
void* p = fun_shared_buffer_data(b);
memcpy(p, src, n); // fill while private
fun_send(port, fun_wrap_shared(b)); // publish + adopt
fun_shared_buffer_release(b); // drop senders ref (optional if send adopts)
// On receiver side
fun_value_t v;
if (fun_recv(port, &v, 1000) == 0) {
fun_shared_buffer_t* r = fun_unwrap_shared(v);
// r is readonly to VMs; parse/process concurrently without locks
// Retain if it must outlive the message scope
fun_shared_buffer_retain(r);
fun_shared_buffer_release(r);
}
```
#### When would the host ever sync?
- Only if you choose shared mutability outside the VM (e.g., a lockfree ring buffer you manage). For that, we expose optional helpers (`fun_atomic_*`, `fun_mutex_t`, `fun_rwlock_t`), but theyre not required for the standard zerocopy path.
#### Guarantees provided by the runtime
- Publication fences around `fun_send`/`fun_recv` ensure bytes written before send are visible to receivers without extra barriers.
- Buffers are pointerfree relative to VM heaps, so no crossheap reachability or GC coordination is needed.
- Refcount updates are atomic; deallocation happens when the global count drops to zero.
#### Bottom line
- You dont synchronize shared data: you publish immutable blobs and let ports handle concurrency and lifetime via atomic refcounting and adoption. Locks are only needed if you intentionally build shared mutable structures outside this model.
### Do perVM GCs need to stoptheworld for shared regions? ### Do perVM GCs need to stoptheworld for shared regions?
It depends on what “shared” means. Our design keeps perVM GCs independent by default: It depends on what “shared” means. Our design keeps perVM GCs independent by default: