1
0
Fork 0
forked from fun/fun

Some more Rusty Fun... :) (0.38.3)

This commit is contained in:
Johannes Findeisen 2026-01-29 03:29:14 +01:00
commit d39ef1a58e
10 changed files with 157 additions and 113 deletions

View file

@ -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)

View file

@ -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
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.
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/).
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.

View file

@ -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 <you@hanez.org>
* 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)!
*/

View file

@ -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 "???";

View file

@ -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)

View file

@ -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; }

View file

@ -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::<u8>::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<u8> 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

View file

@ -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"

View file

@ -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 */

View file

@ -0,0 +1,48 @@
/*
* This file is part of the Fun programming language.
* https://fun-lang.xyz/
*
* Copyright 2026 Johannes Findeisen <you@hanez.org>
* 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;
}