From d39ef1a58ebad8076b5a5f69692157c770695edb Mon Sep 17 00:00:00 2001 From: hanez Date: Thu, 29 Jan 2026 03:29:14 +0100 Subject: [PATCH] Some more Rusty Fun... :) (0.38.3) --- CMakeLists.txt | 2 +- docs/rust.md | 127 ++++------------------------ examples/rust_hello_args_return.fun | 34 ++++++++ src/bytecode.c | 1 + src/bytecode.h | 1 + src/parser.c | 8 ++ src/rust/src/lib.rs | 42 +++++++++ src/vm.c | 1 + src/vm.h | 6 +- src/vm/rust/hello_args_return.c | 48 +++++++++++ 10 files changed, 157 insertions(+), 113 deletions(-) create mode 100644 examples/rust_hello_args_return.fun create mode 100644 src/vm/rust/hello_args_return.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 1e619dc..25f330e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.38.2 LANGUAGES C) +project(fun VERSION 0.38.3 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/docs/rust.md b/docs/rust.md index 9657c1f..faf6ead 100644 --- a/docs/rust.md +++ b/docs/rust.md @@ -1,114 +1,3 @@ -# Writing Rust-backed opcodes for Fun VM - -!!! CAUTION !!! I am not very experienced in using Rust. This should show that it is possible to implement opcodes in Rust and integrate them with the 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 - -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: { @@ -197,3 +86,19 @@ Authoritative and practical resources on exposing Rust to C (FFI) and maintainin 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 + +## Return-only Rust string helper + +Two variants are available for passing a string to Rust and getting output: + +- rust_hello_args(msg) + - Rust side prints the message to stdout; Fun receives Nil. Use when you only want side-effect printing. +- rust_hello_args_return(msg) + - Rust side does not print; it returns the provided string to Fun (useful for assignment or chaining). + +Example: + + msg = rust_hello_args_return("Hello back from Rust (no print)!") + print(msg) + +See examples/rust_hello_args_return.fun for a complete script. diff --git a/examples/rust_hello_args_return.fun b/examples/rust_hello_args_return.fun new file mode 100644 index 0000000..2fd6c6c --- /dev/null +++ b/examples/rust_hello_args_return.fun @@ -0,0 +1,34 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-29 + */ + +/* + * Rust-backed opcode demo with argument: rust_hello_args_return(STRING) + * This variant does NOT print from Rust; it only returns the provided string + * back to Fun for assignment or further processing. + * + * Build instructions: + * - Enable Rust integration: + * cmake -S . -B build_debug -DFUN_WITH_RUST=ON + * - Then build: + * cmake --build build_debug --target fun + * + * Run: + * build_debug/fun examples/rust_hello_args_return.fun + */ + +msg = rust_hello_args_return("Hello back from Rust (no print)!") +print(msg) + +/* Expected output: +Hello back from Rust (no print)! +*/ diff --git a/src/bytecode.c b/src/bytecode.c index ed55ff8..793ef79 100644 --- a/src/bytecode.c +++ b/src/bytecode.c @@ -208,6 +208,7 @@ static const char *opcode_name(OpCode op) { case OP_FMAX: return "FMAX"; case OP_RUST_HELLO: return "RUST_HELLO"; case OP_RUST_HELLO_ARGS: return "RUST_HELLO_ARGS"; + case OP_RUST_HELLO_ARGS_RETURN: return "RUST_HELLO_ARGS_RETURN"; case OP_RUST_GET_SP: return "RUST_GET_SP"; case OP_RUST_SET_EXIT: return "RUST_SET_EXIT"; default: return "???"; diff --git a/src/bytecode.h b/src/bytecode.h index 4fbf9ef..9731664 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -266,6 +266,7 @@ typedef enum { // Rust FFI demo opcode(s) OP_RUST_HELLO, // pushes string returned from Rust (hello world) OP_RUST_HELLO_ARGS, // pops message string; prints it via Rust; pushes Nil + OP_RUST_HELLO_ARGS_RETURN, // pops message string; returns it from Rust without printing; pushes returned string OP_RUST_GET_SP, // pushes current VM stack pointer (via Rust reading VM memory) OP_RUST_SET_EXIT, // pops int and sets VM exit_code (via Rust writing VM memory) diff --git a/src/parser.c b/src/parser.c index 78697a7..14a44d5 100644 --- a/src/parser.c +++ b/src/parser.c @@ -801,6 +801,14 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } + if (strcmp(name, "rust_hello_args_return") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "rust_hello_args_return expects (message:string)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after rust_hello_args_return arg"); free(name); return 0; } + bytecode_add_instruction(bc, OP_RUST_HELLO_ARGS_RETURN, 0); + free(name); + return 1; + } if (strcmp(name, "rust_get_sp") == 0) { (*pos)++; /* '(' */ if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "rust_get_sp expects ()"); free(name); return 0; } diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs index fbd9be7..b077762 100644 --- a/src/rust/src/lib.rs +++ b/src/rust/src/lib.rs @@ -48,4 +48,46 @@ pub extern "C" fn fun_rust_print_string(msg: *const core::ffi::c_char) -> i32 { 0 } +// Return a newly allocated duplicate of the given C string. +// Caller (C side) must free using fun_rust_string_free. +#[no_mangle] +pub extern "C" fn fun_rust_echo_string(input: *const core::ffi::c_char) -> *mut core::ffi::c_char { + use core::ffi::CStr; + unsafe { + if input.is_null() { + // allocate empty string + let v = Vec::::from([0u8]); + return v.leak().as_mut_ptr() as *mut core::ffi::c_char; + } + match CStr::from_ptr(input).to_str() { + Ok(s) => { + let mut v = s.as_bytes().to_vec(); + v.push(0); // NUL-terminate + v.leak().as_mut_ptr() as *mut core::ffi::c_char + } + Err(_) => { + // On invalid UTF-8, still duplicate bytes as-is + let c = CStr::from_ptr(input); + let mut v = c.to_bytes().to_vec(); + v.push(0); + v.leak().as_mut_ptr() as *mut core::ffi::c_char + } + } + } +} + +// Free a C string previously returned by fun_rust_echo_string +#[no_mangle] +pub extern "C" fn fun_rust_string_free(ptr: *mut core::ffi::c_char) { + if ptr.is_null() { return; } + unsafe { + // Reconstruct a Vec so Rust will free it when it drops + // Determine length by scanning for NUL + let mut len: usize = 0; + while *ptr.add(len) != 0 { len += 1; } + let slice = core::slice::from_raw_parts_mut(ptr as *mut u8, len + 1); + let _ = Vec::from_raw_parts(slice.as_mut_ptr(), len + 1, len + 1); + } +} + // No custom panic handler; use std default diff --git a/src/vm.c b/src/vm.c index 46c451e..3464f60 100644 --- a/src/vm.c +++ b/src/vm.c @@ -789,6 +789,7 @@ void vm_run(VM *vm, Bytecode *entry) { /* Rust FFI demo opcode(s) */ #include "vm/rust/hello.c" #include "vm/rust/hello_args.c" + #include "vm/rust/hello_args_return.c" #include "vm/rust/get_sp.c" #include "vm/rust/set_exit.c" diff --git a/src/vm.h b/src/vm.h index 1503b0d..05a94f2 100644 --- a/src/vm.h +++ b/src/vm.h @@ -57,7 +57,7 @@ static const char *opcode_names[] = { "TRY_PUSH","TRY_POP","THROW", "FMIN","FMAX", /* Rust FFI demo */ - "RUST_HELLO","RUST_HELLO_ARGS","RUST_GET_SP","RUST_SET_EXIT", + "RUST_HELLO","RUST_HELLO_ARGS","RUST_HELLO_ARGS_RETURN","RUST_GET_SP","RUST_SET_EXIT", /* Notcurses TUI (optional) */ "NC_INIT","NC_SHUTDOWN","NC_CLEAR","NC_DRAW_TEXT","NC_GETCH" }; @@ -160,6 +160,10 @@ int fun_op_radd(VM *vm); const char *fun_rust_get_string(void); /* Rust function that prints a passed C string, returns 0 on success. */ int fun_rust_print_string(const char *msg); +/* Rust function that returns a newly allocated duplicate of the input C string. */ +char *fun_rust_echo_string(const char *input); +/* Free a C string previously returned by fun_rust_echo_string. */ +void fun_rust_string_free(char *ptr); /* --- Extended C ABI for Rust to access VM internals (unsafe) --- */ /* Size helpers for Rust side to compute offsets and do pointer math */ diff --git a/src/vm/rust/hello_args_return.c b/src/vm/rust/hello_args_return.c new file mode 100644 index 0000000..6adf44d --- /dev/null +++ b/src/vm/rust/hello_args_return.c @@ -0,0 +1,48 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-29 + */ + +/* + * Rust FFI demo opcode with argument: OP_RUST_HELLO_ARGS_RETURN + * Pops a string from the stack, asks Rust to return it back (no printing). + * Pushes the returned string, or Nil on error. + */ +case OP_RUST_HELLO_ARGS_RETURN: { +#ifdef FUN_WITH_RUST + Value vmsg = pop_value(vm); + char *msg = value_to_string_alloc(&vmsg); + free_value(vmsg); + if (msg) { + char *ret = fun_rust_echo_string(msg); + free(msg); + if (ret) { + push_value(vm, make_string(ret)); + fun_rust_string_free(ret); + } else { + push_value(vm, make_nil()); + } + } else { + char *ret = fun_rust_echo_string(""); + if (ret) { + push_value(vm, make_string(ret)); + fun_rust_string_free(ret); + } else { + push_value(vm, make_nil()); + } + } +#else + /* Still pop and free the arg to keep stack sane */ + Value vmsg = pop_value(vm); + free_value(vmsg); + vm_raise_error(vm, "RUST_HELLO_ARGS_RETURN requires FUN_WITH_RUST=ON at build time"); + push_value(vm, make_nil()); +#endif + break; +}