diff --git a/docs/rust.md b/docs/rust.md index cb255f0..71ca53b 100644 --- a/docs/rust.md +++ b/docs/rust.md @@ -1,197 +1,205 @@ # 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. - - It assumes you are comfortable with basic Rust and C and have a working Fun checkout. - - ## Overview - - Fun’s 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. - 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 VM’s 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. - - ## Project layout (relevant parts) - - - 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. - - 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). - - ## Enabling Rust in the build - - 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): - - - Debug example: - cmake -S . -B build_debug -DFUN_WITH_RUST=ON - cmake --build build_debug --target fun - - - Release example: - cmake -S . -B build_release -DFUN_WITH_RUST=ON - cmake --build build_release --target fun - - Useful targets in this repository include: - - fun — the main executable - - rust_ops_build — helps build/link Rust ops when enabled - - 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. - - ## Writing an opcode in Rust - +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. + +## Overview + +Fun’s 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. +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 VM’s 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. + +## Project layout (relevant parts) + +- 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. +- 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). + +## Enabling Rust in the build + +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): + +- Debug example: + cmake -S . -B build_debug -DFUN_WITH_RUST=ON + cmake --build build_debug --target fun + +- Release example: + cmake -S . -B build_release -DFUN_WITH_RUST=ON + cmake --build build_release --target fun + +Useful targets in this repository include: +- fun — the main executable +- rust_ops_build — helps build/link Rust ops when enabled +- 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. + +## 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. - - Key points: - - 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). - - Interact with the VM stack via helper FFI functions declared as externs. - - Provide a minimal panic handler (no_std) as shown in lib.rs. - - Example: integer addition opcode implemented in Rust. - - In src/rust/src/lib.rs: - - #![no_std] - - #[repr(C)] - pub struct Vm; - - extern "C" { - fn vm_pop_i64(vm: *mut Vm) -> i64; - fn vm_push_i64(vm: *mut Vm, v: i64); - } - - #[no_mangle] - pub extern "C" fn fun_op_radd(vm: *mut Vm) -> i32 { - unsafe { - let b = vm_pop_i64(vm); - let a = vm_pop_i64(vm); - vm_push_i64(vm, a + b); - } - 0 - } - - #[panic_handler] - fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} } - - What this does: - - Pops two 64-bit integers from the VM stack. - - Pushes back their sum. - - 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. - - ## 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/. - - String demo wiring (already present): src/vm/rust/hello.c - - case OP_RUST_HELLO: { - #ifdef FUN_WITH_RUST - const char *s = fun_rust_get_string(); - if (!s) s = ""; - push_value(vm, make_string(s)); - #else - vm_raise_error(vm, "RUST_HELLO requires FUN_WITH_RUST=ON at build time"); - push_value(vm, make_nil()); - #endif - break; - } - - 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 - #endif - - case OP_RADD: { - #ifdef FUN_WITH_RUST - (void)fun_op_radd(vm); - #else - vm_raise_error(vm, "RADD requires FUN_WITH_RUST=ON at build time"); - push_value(vm, make_nil()); // or follow your opcode’s error convention - #endif - break; - } - - 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 - - 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. - - Run the example: - - 1) Build with Rust enabled (Debug): - cmake -S . -B build_debug -DFUN_WITH_RUST=ON - cmake --build build_debug --target fun - - 2) Execute the script: - build_debug/fun examples/rust_hello.fun - - Expected output: - Hello from Rust ops! - - If you build without Rust, calling rust_hello() raises a runtime error indicating that Rust integration is disabled. - - ## Stack discipline and error handling - - - 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 VM’s error mechanism (e.g., vm_raise_error) and follow the module’s convention on what to push after errors. - - ## Data types and FFI surface - - The minimal helpers shown cover 64-bit integers and simple strings. Extending the Rust<->C bridge usually involves: - - 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 - - - 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. - - Missing symbol at runtime: Confirm #[no_mangle] and extern "C" on the Rust function and that C sees the correct prototype. - - Wrong or garbled values: Double-check stack order (Fun uses a stack VM; many ops pop in reverse order: first b, then a). - - 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 - - 1) Write the Rust function in src/rust/src/lib.rs with extern "C", #[no_mangle]. - 2) Use FFI helpers to pop arguments and push results. - 3) Add a C-side case under src/vm/... (or src/vm/rust/...) that calls your Rust function when the opcode executes. - 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. - - ## References in this repo - - - 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 - - ## Links - - Authoritative and practical resources on exposing Rust to C (FFI) and maintaining a C-compatible API: - - - Rustonomicon: FFI overview and best practices - https://doc.rust-lang.org/nomicon/ffi.html - - Rustonomicon: Calling Rust code from C - 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) - https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#calling-rust-functions-from-other-languages - - Rust Reference: extern blocks, ABIs, and linkage - https://doc.rust-lang.org/reference/items/external-blocks.html - - Rust FFI Omnibus (examples for many patterns, including Rust ↔ C) - 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 + +Key points: +- 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). +- Interact with the VM stack via helper FFI functions declared as externs. +- Provide a minimal panic handler (no_std) as shown in lib.rs. + +Example: integer addition opcode implemented in Rust. + +In src/rust/src/lib.rs: + +``` +#![no_std] + + #[repr(C)] + pub struct Vm; + + extern "C" { + fn vm_pop_i64(vm: *mut Vm) -> i64; + fn vm_push_i64(vm: *mut Vm, v: i64); + } + + #[no_mangle] + pub extern "C" fn fun_op_radd(vm: *mut Vm) -> i32 { + unsafe { + let b = vm_pop_i64(vm); + let a = vm_pop_i64(vm); + vm_push_i64(vm, a + b); + } + 0 + } + + #[panic_handler] + fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} } +``` + +What this does: +- Pops two 64-bit integers from the VM stack. +- Pushes back their sum. +- 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. + +## 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/. + +String demo wiring (already present): src/vm/rust/hello.c + +``` +case OP_RUST_HELLO: { +#ifdef FUN_WITH_RUST + const char *s = fun_rust_get_string(); + if (!s) s = ""; + push_value(vm, make_string(s)); +#else + vm_raise_error(vm, "RUST_HELLO requires FUN_WITH_RUST=ON at build time"); + push_value(vm, make_nil()); +#endif + break; +} +``` + +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 +#endif + +case OP_RADD: { +#ifdef FUN_WITH_RUST + (void)fun_op_radd(vm); +#else + vm_raise_error(vm, "RADD requires FUN_WITH_RUST=ON at build time"); + push_value(vm, make_nil()); // or follow your opcode’s error convention +#endif + break; +} +``` + +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 + +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. + +Run the example: + +1) Build with Rust enabled (Debug): + cmake -S . -B build_debug -DFUN_WITH_RUST=ON + cmake --build build_debug --target fun + +2) Execute the script: + build_debug/fun examples/rust_hello.fun + +Expected output: + Hello from Rust ops! + +If you build without Rust, calling rust_hello() raises a runtime error indicating that Rust integration is disabled. + +## Stack discipline and error handling + +- 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 VM’s error mechanism (e.g., vm_raise_error) and follow the module’s convention on what to push after errors. + +## Data types and FFI surface + +The minimal helpers shown cover 64-bit integers and simple strings. Extending the Rust<->C bridge usually involves: + +- 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 + +- 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. +- Missing symbol at runtime: Confirm #[no_mangle] and extern "C" on the Rust function and that C sees the correct prototype. +- Wrong or garbled values: Double-check stack order (Fun uses a stack VM; many ops pop in reverse order: first b, then a). +- 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 + +1) Write the Rust function in src/rust/src/lib.rs with extern "C", #[no_mangle]. +2) Use FFI helpers to pop arguments and push results. +3) Add a C-side case under src/vm/... (or src/vm/rust/...) that calls your Rust function when the opcode executes. +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. + +## References in this repo + +- 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 + +## Links + +Authoritative and practical resources on exposing Rust to C (FFI) and maintaining a C-compatible API: + +- Rustonomicon: FFI overview and best practices + https://doc.rust-lang.org/nomicon/ffi.html +- Rustonomicon: Calling Rust code from C + 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) + https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#calling-rust-functions-from-other-languages +- Rust Reference: extern blocks, ABIs, and linkage + https://doc.rust-lang.org/reference/items/external-blocks.html +- Rust FFI Omnibus (examples for many patterns, including Rust ↔ C) + 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