1
0
Fork 0
forked from fun/fun

Rust doc fixes. No code changes. (0.38.5)

This commit is contained in:
Johannes Findeisen 2026-01-29 15:17:46 +01:00
commit c03ac29e40

View file

@ -1,197 +1,205 @@
# Writing Rust-backed opcodes for Fun VM # Writing Rust-backed opcodes for Fun VM
This guide explains how to implement VM opcodes in Rust, wire them into the C VM, and use them from Fun scripts. This guide explains how to implement VM opcodes in Rust, wire them into the C VM, and use them from Fun scripts.
It assumes you are comfortable with basic Rust and C and have a working Fun checkout. It assumes you are comfortable with basic Rust and C and have a working Fun checkout.
## Overview ## Overview
Funs VM is written in C, but you can implement opcode handlers in Rust and call them via FFI. The typical flow is: Funs VM is written in C, but you can implement opcode handlers in Rust and call them via FFI. The typical flow is:
1) Write a Rust function with a stable C ABI (extern "C", #[no_mangle]) that takes a pointer to the VM and returns an int status code. 1) Write a Rust function with a stable C ABI (extern "C", #[no_mangle]) that takes a pointer to the VM and returns an int status code.
2) Use VM stack helpers (exposed to Rust via FFI) to pop arguments and push results. 2) Use VM stack helpers (exposed to Rust via FFI) to pop arguments and push results.
3) Expose that Rust function to the C VM by calling it from the opcode dispatch (a case in the VMs opcode switch or a small shim under src/vm/rust/). 3) Expose that Rust function to the C VM by calling it from the opcode dispatch (a case in the VMs opcode switch or a small shim under src/vm/rust/).
4) Add or reuse a Fun builtin that maps to your opcode, then call it from Fun code. 4) Add or reuse a Fun builtin that maps to your opcode, then call it from Fun code.
## Project layout (relevant parts) ## Project layout (relevant parts)
- src/rust/src/lib.rs — Rust library with exported opcode functions and FFI helpers. - src/rust/src/lib.rs — Rust library with exported opcode functions and FFI helpers.
- src/vm/rust/ — C-side wiring examples and small opcode cases calling into Rust. - src/vm/rust/ — C-side wiring examples and small opcode cases calling into Rust.
- examples/rust_hello.fun — Example Fun script using a Rust-backed opcode. - examples/rust_hello.fun — Example Fun script using a Rust-backed opcode.
- docs/opcodes.md — General overview of many built-in opcodes (mostly C-based). - docs/opcodes.md — General overview of many built-in opcodes (mostly C-based).
## Enabling Rust in the build ## Enabling Rust in the build
Rust integration is optional and gated by a CMake flag. Default builds usually have it OFF. Rust integration is optional and gated by a CMake flag. Default builds usually have it OFF.
Enable it for a configured profile (Debug or Release): Enable it for a configured profile (Debug or Release):
- Debug example: - Debug example:
cmake -S . -B build_debug -DFUN_WITH_RUST=ON cmake -S . -B build_debug -DFUN_WITH_RUST=ON
cmake --build build_debug --target fun cmake --build build_debug --target fun
- Release example: - Release example:
cmake -S . -B build_release -DFUN_WITH_RUST=ON cmake -S . -B build_release -DFUN_WITH_RUST=ON
cmake --build build_release --target fun cmake --build build_release --target fun
Useful targets in this repository include: Useful targets in this repository include:
- fun — the main executable - fun — the main executable
- rust_ops_build — helps build/link Rust ops when enabled - rust_ops_build — helps build/link Rust ops when enabled
- test_opcodes — test executable (if you want to extend tests) - test_opcodes — test executable (if you want to extend tests)
Note: In CLion, prefer building with one of the provided CMake profiles (Debug/Release) and avoid creating custom build directories. Note: In CLion, prefer building with one of the provided CMake profiles (Debug/Release) and avoid creating custom build directories.
## Writing an opcode in Rust ## Writing an opcode in Rust
The Rust side is a no_std static library exposing C ABI functions that the VM can call. See src/rust/src/lib.rs for examples already in the tree. The Rust side is a no_std static library exposing C ABI functions that the VM can call. See src/rust/src/lib.rs for examples already in the tree.
Key points: Key points:
- Use extern "C" and #[no_mangle] to fix the symbol name. - Use extern "C" and #[no_mangle] to fix the symbol name.
- Take a raw pointer to the VM as *mut Vm; return i32 status (0 for success). - Take a raw pointer to the VM as *mut Vm; return i32 status (0 for success).
- Interact with the VM stack via helper FFI functions declared as externs. - Interact with the VM stack via helper FFI functions declared as externs.
- Provide a minimal panic handler (no_std) as shown in lib.rs. - Provide a minimal panic handler (no_std) as shown in lib.rs.
Example: integer addition opcode implemented in Rust. Example: integer addition opcode implemented in Rust.
In src/rust/src/lib.rs: In src/rust/src/lib.rs:
#![no_std] ```
#![no_std]
#[repr(C)] #[repr(C)]
pub struct Vm; pub struct Vm;
extern "C" { extern "C" {
fn vm_pop_i64(vm: *mut Vm) -> i64; fn vm_pop_i64(vm: *mut Vm) -> i64;
fn vm_push_i64(vm: *mut Vm, v: i64); fn vm_push_i64(vm: *mut Vm, v: i64);
} }
#[no_mangle] #[no_mangle]
pub extern "C" fn fun_op_radd(vm: *mut Vm) -> i32 { pub extern "C" fn fun_op_radd(vm: *mut Vm) -> i32 {
unsafe { unsafe {
let b = vm_pop_i64(vm); let b = vm_pop_i64(vm);
let a = vm_pop_i64(vm); let a = vm_pop_i64(vm);
vm_push_i64(vm, a + b); vm_push_i64(vm, a + b);
} }
0 0
} }
#[panic_handler] #[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} } fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} }
```
What this does: What this does:
- Pops two 64-bit integers from the VM stack. - Pops two 64-bit integers from the VM stack.
- Pushes back their sum. - Pushes back their sum.
- Returns 0 to indicate success to the VM. - Returns 0 to indicate success to the VM.
You can add more extern helpers (e.g., for strings, arrays, maps) once they are exposed by the C VM. The repository already includes a simple string example returning a const char* from Rust, see fun_rust_get_string() usage below. You can add more extern helpers (e.g., for strings, arrays, maps) once they are exposed by the C VM. The repository already includes a simple string example returning a const char* from Rust, see fun_rust_get_string() usage below.
## Wiring the opcode in C ## Wiring the opcode in C
To make the VM call your Rust opcode, add a small C-side case that invokes the exported Rust symbol. A minimal pattern lives under src/vm/rust/. To make the VM call your Rust opcode, add a small C-side case that invokes the exported Rust symbol. A minimal pattern lives under src/vm/rust/.
String demo wiring (already present): src/vm/rust/hello.c String demo wiring (already present): src/vm/rust/hello.c
case OP_RUST_HELLO: { ```
#ifdef FUN_WITH_RUST case OP_RUST_HELLO: {
const char *s = fun_rust_get_string(); #ifdef FUN_WITH_RUST
if (!s) s = ""; const char *s = fun_rust_get_string();
push_value(vm, make_string(s)); if (!s) s = "";
#else push_value(vm, make_string(s));
vm_raise_error(vm, "RUST_HELLO requires FUN_WITH_RUST=ON at build time"); #else
push_value(vm, make_nil()); vm_raise_error(vm, "RUST_HELLO requires FUN_WITH_RUST=ON at build time");
#endif push_value(vm, make_nil());
break; #endif
} break;
}
```
For a stack-based math opcode (like fun_op_radd), you would declare and call the Rust function similarly: For a stack-based math opcode (like fun_op_radd), you would declare and call the Rust function similarly:
#ifdef FUN_WITH_RUST ```
extern int fun_op_radd(void* vm); // or use the proper VM type if available #ifdef FUN_WITH_RUST
#endif extern int fun_op_radd(void* vm); // or use the proper VM type if available
#endif
case OP_RADD: { case OP_RADD: {
#ifdef FUN_WITH_RUST #ifdef FUN_WITH_RUST
(void)fun_op_radd(vm); (void)fun_op_radd(vm);
#else #else
vm_raise_error(vm, "RADD requires FUN_WITH_RUST=ON at build time"); vm_raise_error(vm, "RADD requires FUN_WITH_RUST=ON at build time");
push_value(vm, make_nil()); // or follow your opcodes error convention push_value(vm, make_nil()); // or follow your opcodes error convention
#endif #endif
break; break;
} }
```
Notes: Notes:
- Follow the existing opcode conventions for your module (core, math, strings, etc.).
- If your build puts the Rust symbol into a static library, make sure the VM target links it when FUN_WITH_RUST is ON (the top-level CMake already does this for the examples provided).
## Using Rust-backed opcodes from Fun - Follow the existing opcode conventions for your module (core, math, strings, etc.).
- If your build puts the Rust symbol into a static library, make sure the VM target links it when FUN_WITH_RUST is ON (the top-level CMake already does this for the examples provided).
Once wired, expose the opcode via a builtin function or directly in bytecode. The repository includes a demo builtin rust_hello() that returns a Rust-generated string. ## Using Rust-backed opcodes from Fun
Run the example: Once wired, expose the opcode via a builtin function or directly in bytecode. The repository includes a demo builtin rust_hello() that returns a Rust-generated string.
1) Build with Rust enabled (Debug): Run the example:
cmake -S . -B build_debug -DFUN_WITH_RUST=ON
cmake --build build_debug --target fun
2) Execute the script: 1) Build with Rust enabled (Debug):
build_debug/fun examples/rust_hello.fun cmake -S . -B build_debug -DFUN_WITH_RUST=ON
cmake --build build_debug --target fun
Expected output: 2) Execute the script:
Hello from Rust ops! build_debug/fun examples/rust_hello.fun
If you build without Rust, calling rust_hello() raises a runtime error indicating that Rust integration is disabled. Expected output:
Hello from Rust ops!
## Stack discipline and error handling If you build without Rust, calling rust_hello() raises a runtime error indicating that Rust integration is disabled.
- Always pop exactly the arguments you expect and push exactly the results your opcode promises. Mismatch leads to stack corruption and hard-to-debug failures. ## Stack discipline and error handling
- Return an int status to the VM (0 for success). If your project uses a different convention for some opcodes, match it consistently.
- Validate types where appropriate (e.g., ensure values are integers before arithmetic). If a check fails, use the VMs error mechanism (e.g., vm_raise_error) and follow the modules convention on what to push after errors.
## Data types and FFI surface - Always pop exactly the arguments you expect and push exactly the results your opcode promises. Mismatch leads to stack corruption and hard-to-debug failures.
- Return an int status to the VM (0 for success). If your project uses a different convention for some opcodes, match it consistently.
- Validate types where appropriate (e.g., ensure values are integers before arithmetic). If a check fails, use the VMs error mechanism (e.g., vm_raise_error) and follow the modules convention on what to push after errors.
The minimal helpers shown cover 64-bit integers and simple strings. Extending the Rust<->C bridge usually involves: ## Data types and FFI surface
- Declaring additional extern "C" functions in Rust that the C VM implements (to read/write values on the stack, construct arrays/maps/strings, etc.).
- Ensuring all pointers and lifetimes are well-defined: strings pushed to the VM should be copied or allocated using VM facilities so they remain valid after the call.
- Keeping Rust no_std unless you add an allocator and link setup to support std.
## Troubleshooting The minimal helpers shown cover 64-bit integers and simple strings. Extending the Rust<->C bridge usually involves:
- Link errors: Make sure FUN_WITH_RUST=ON for your build directory and that the Rust library is compiled before linking the VM. Use the rust_ops_build target if provided by your profile. - Declaring additional extern "C" functions in Rust that the C VM implements (to read/write values on the stack, construct arrays/maps/strings, etc.).
- Missing symbol at runtime: Confirm #[no_mangle] and extern "C" on the Rust function and that C sees the correct prototype. - Ensuring all pointers and lifetimes are well-defined: strings pushed to the VM should be copied or allocated using VM facilities so they remain valid after the call.
- Wrong or garbled values: Double-check stack order (Fun uses a stack VM; many ops pop in reverse order: first b, then a). - Keeping Rust no_std unless you add an allocator and link setup to support std.
- No output from rust_hello(): Ensure you run a binary built with FUN_WITH_RUST=ON; otherwise the VM deliberately raises an error and returns Nil for that call.
## Small end-to-end checklist ## Troubleshooting
1) Write the Rust function in src/rust/src/lib.rs with extern "C", #[no_mangle]. - Link errors: Make sure FUN_WITH_RUST=ON for your build directory and that the Rust library is compiled before linking the VM. Use the rust_ops_build target if provided by your profile.
2) Use FFI helpers to pop arguments and push results. - Missing symbol at runtime: Confirm #[no_mangle] and extern "C" on the Rust function and that C sees the correct prototype.
3) Add a C-side case under src/vm/... (or src/vm/rust/...) that calls your Rust function when the opcode executes. - Wrong or garbled values: Double-check stack order (Fun uses a stack VM; many ops pop in reverse order: first b, then a).
4) Ensure the build links Rust code when FUN_WITH_RUST=ON. - No output from rust_hello(): Ensure you run a binary built with FUN_WITH_RUST=ON; otherwise the VM deliberately raises an error and returns Nil for that call.
5) Add or reuse a builtin in the parser/runtime to surface your opcode to Fun code.
6) Build and run a small .fun example to validate behavior.
## References in this repo ## Small end-to-end checklist
- Rust lib with examples: src/rust/src/lib.rs 1) Write the Rust function in src/rust/src/lib.rs with extern "C", #[no_mangle].
- C-side hello wiring: src/vm/rust/hello.c 2) Use FFI helpers to pop arguments and push results.
- Demo script: examples/rust_hello.fun 3) Add a C-side case under src/vm/... (or src/vm/rust/...) that calls your Rust function when the opcode executes.
- General opcode reference: docs/opcodes.md 4) Ensure the build links Rust code when FUN_WITH_RUST=ON.
5) Add or reuse a builtin in the parser/runtime to surface your opcode to Fun code.
6) Build and run a small .fun example to validate behavior.
## Links ## References in this repo
Authoritative and practical resources on exposing Rust to C (FFI) and maintaining a C-compatible API: - Rust lib with examples: src/rust/src/lib.rs
- C-side hello wiring: src/vm/rust/hello.c
- Demo script: examples/rust_hello.fun
- General opcode reference: docs/opcodes.md
- Rustonomicon: FFI overview and best practices ## Links
https://doc.rust-lang.org/nomicon/ffi.html
- Rustonomicon: Calling Rust code from C Authoritative and practical resources on exposing Rust to C (FFI) and maintaining a C-compatible API:
https://doc.rust-lang.org/nomicon/ffi.html#calling-rust-code-from-c
- The Rust Book: Unsafe and FFI (extern, #[no_mangle], calling Rust from other languages) - Rustonomicon: FFI overview and best practices
https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#calling-rust-functions-from-other-languages https://doc.rust-lang.org/nomicon/ffi.html
- Rust Reference: extern blocks, ABIs, and linkage - Rustonomicon: Calling Rust code from C
https://doc.rust-lang.org/reference/items/external-blocks.html https://doc.rust-lang.org/nomicon/ffi.html#calling-rust-code-from-c
- Rust FFI Omnibus (examples for many patterns, including Rust ↔ C) - The Rust Book: Unsafe and FFI (extern, #[no_mangle], calling Rust from other languages)
https://github.com/shepmaster/rust-ffi-omnibus https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#calling-rust-functions-from-other-languages
- cbindgen (generate C headers from Rust libraries) - Rust Reference: extern blocks, ABIs, and linkage
https://cbindgen.github.io/cbindgen/ https://doc.rust-lang.org/reference/items/external-blocks.html
- bindgen (generate Rust bindings to existing C headers; useful when mixing C and Rust) - Rust FFI Omnibus (examples for many patterns, including Rust ↔ C)
https://github.com/rust-lang/rust-bindgen https://github.com/shepmaster/rust-ffi-omnibus
- cbindgen (generate C headers from Rust libraries)
https://cbindgen.github.io/cbindgen/
- bindgen (generate Rust bindings to existing C headers; useful when mixing C and Rust)
https://github.com/rust-lang/rust-bindgen