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
### 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.
### 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.
### 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?
It depends on what “shared” means. Our design keeps perVM GCs independent by default: