Some more Rusty Fun... :) (0.38.3)
This commit is contained in:
parent
c2e7f3869e
commit
d39ef1a58e
10 changed files with 157 additions and 113 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
cmake_minimum_required(VERSION 3.10)
|
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 99)
|
||||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||||
|
|
|
||||||
127
docs/rust.md
127
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
|
#endif
|
||||||
|
|
||||||
case OP_RADD: {
|
case OP_RADD: {
|
||||||
|
|
@ -197,3 +86,19 @@ Authoritative and practical resources on exposing Rust to C (FFI) and maintainin
|
||||||
https://cbindgen.github.io/cbindgen/
|
https://cbindgen.github.io/cbindgen/
|
||||||
- bindgen (generate Rust bindings to existing C headers; useful when mixing C and Rust)
|
- bindgen (generate Rust bindings to existing C headers; useful when mixing C and Rust)
|
||||||
https://github.com/rust-lang/rust-bindgen
|
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.
|
||||||
|
|
|
||||||
34
examples/rust_hello_args_return.fun
Normal file
34
examples/rust_hello_args_return.fun
Normal 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)!
|
||||||
|
*/
|
||||||
|
|
@ -208,6 +208,7 @@ static const char *opcode_name(OpCode op) {
|
||||||
case OP_FMAX: return "FMAX";
|
case OP_FMAX: return "FMAX";
|
||||||
case OP_RUST_HELLO: return "RUST_HELLO";
|
case OP_RUST_HELLO: return "RUST_HELLO";
|
||||||
case OP_RUST_HELLO_ARGS: return "RUST_HELLO_ARGS";
|
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_GET_SP: return "RUST_GET_SP";
|
||||||
case OP_RUST_SET_EXIT: return "RUST_SET_EXIT";
|
case OP_RUST_SET_EXIT: return "RUST_SET_EXIT";
|
||||||
default: return "???";
|
default: return "???";
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,7 @@ typedef enum {
|
||||||
// Rust FFI demo opcode(s)
|
// Rust FFI demo opcode(s)
|
||||||
OP_RUST_HELLO, // pushes string returned from Rust (hello world)
|
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, // 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_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)
|
OP_RUST_SET_EXIT, // pops int and sets VM exit_code (via Rust writing VM memory)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -801,6 +801,14 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos)
|
||||||
free(name);
|
free(name);
|
||||||
return 1;
|
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) {
|
if (strcmp(name, "rust_get_sp") == 0) {
|
||||||
(*pos)++; /* '(' */
|
(*pos)++; /* '(' */
|
||||||
if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "rust_get_sp expects ()"); free(name); return 0; }
|
if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "rust_get_sp expects ()"); free(name); return 0; }
|
||||||
|
|
|
||||||
|
|
@ -48,4 +48,46 @@ pub extern "C" fn fun_rust_print_string(msg: *const core::ffi::c_char) -> i32 {
|
||||||
0
|
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
|
// No custom panic handler; use std default
|
||||||
|
|
|
||||||
1
src/vm.c
1
src/vm.c
|
|
@ -789,6 +789,7 @@ void vm_run(VM *vm, Bytecode *entry) {
|
||||||
/* Rust FFI demo opcode(s) */
|
/* Rust FFI demo opcode(s) */
|
||||||
#include "vm/rust/hello.c"
|
#include "vm/rust/hello.c"
|
||||||
#include "vm/rust/hello_args.c"
|
#include "vm/rust/hello_args.c"
|
||||||
|
#include "vm/rust/hello_args_return.c"
|
||||||
#include "vm/rust/get_sp.c"
|
#include "vm/rust/get_sp.c"
|
||||||
#include "vm/rust/set_exit.c"
|
#include "vm/rust/set_exit.c"
|
||||||
|
|
||||||
|
|
|
||||||
6
src/vm.h
6
src/vm.h
|
|
@ -57,7 +57,7 @@ static const char *opcode_names[] = {
|
||||||
"TRY_PUSH","TRY_POP","THROW",
|
"TRY_PUSH","TRY_POP","THROW",
|
||||||
"FMIN","FMAX",
|
"FMIN","FMAX",
|
||||||
/* Rust FFI demo */
|
/* 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) */
|
/* Notcurses TUI (optional) */
|
||||||
"NC_INIT","NC_SHUTDOWN","NC_CLEAR","NC_DRAW_TEXT","NC_GETCH"
|
"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);
|
const char *fun_rust_get_string(void);
|
||||||
/* Rust function that prints a passed C string, returns 0 on success. */
|
/* Rust function that prints a passed C string, returns 0 on success. */
|
||||||
int fun_rust_print_string(const char *msg);
|
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) --- */
|
/* --- Extended C ABI for Rust to access VM internals (unsafe) --- */
|
||||||
/* Size helpers for Rust side to compute offsets and do pointer math */
|
/* Size helpers for Rust side to compute offsets and do pointer math */
|
||||||
|
|
|
||||||
48
src/vm/rust/hello_args_return.c
Normal file
48
src/vm/rust/hello_args_return.c
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue