Idiomatic high-level Janet bindings for Rust
Find a file
2025-10-14 15:44:26 -04:00
.vscode feat: add bindgen bindings 2025-10-03 01:37:29 -04:00
csrc Initial commit, nothing works yet 2025-10-03 01:23:07 -04:00
examples feat: add function invocations 2025-10-11 19:45:45 +02:00
src fix: use VM core environment during unmarshal 2025-10-14 15:44:26 -04:00
.envrc Initial commit, nothing works yet 2025-10-03 01:23:07 -04:00
.gitignore Initial commit, nothing works yet 2025-10-03 01:23:07 -04:00
build.rs feat: add bindgen bindings 2025-10-03 01:37:29 -04:00
Cargo.lock feat: table support 2025-10-10 17:26:52 +02:00
Cargo.toml feat: table support 2025-10-10 17:26:52 +02:00
flake.lock Initial commit, nothing works yet 2025-10-03 01:23:07 -04:00
flake.nix Initial commit, nothing works yet 2025-10-03 01:23:07 -04:00
LICENSE Initial commit, nothing works yet 2025-10-03 01:23:07 -04:00
README.md feat: add Functions and CFunctions support 2025-10-05 08:43:23 -04:00
update-janet.sh Initial commit, nothing works yet 2025-10-03 01:23:07 -04:00

janet-binds

Idiomatic Rust bindings for the Janet programming language.

Status

Active development. Core functionality and basic collections are implemented.

What Works

  • Janet VM initialization and cleanup
  • Code evaluation via VM::run()
  • Value type inspection and conversion
  • Basic types: nil, boolean, number, string
  • Symbols and keywords
  • Collections: arrays, tuples, tables, structs
  • Collection iteration with Rust iterators
  • Safe memory management via RAII
  • Calling Janet functions from Rust via Value::call(), VM::call_function()
  • Function type checking via is_function(), is_cfunction()
  • Registering Rust functions as Janet CFunctions via janet_fn!, janet_fns! macros
  • Prefix-based function registration, group related functions under namespaces

What's Missing

  • Fibers (coroutines)
  • Abstract types
  • Error stack traces (partially done, error messages included)
  • Module system (dofile, import)

Usage

use janet_binds::{VM, Value, Array, Table, Symbol, Keyword, janet_array, janet_tuple};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let vm = VM::new()?;

    // Evaluate Janet code
    let result = vm.run("(+ 1 2 3)")?;
    assert_eq!(result.as_i32()?, 6);

    // Call Janet functions from Rust
    vm.run("(defn square [x] (* x x))")?;

    // Method 1: Get function value and call it
    let square_fn = vm.get_global("square").unwrap();
    let result = square_fn.call(&[5.into()])?;
    assert_eq!(result.as_i32()?, 25);

    // Method 2: Call by name
    let result = vm.call_function("square", &[7.into()])?;
    assert_eq!(result.as_i32()?, 49);

    // Call built-in functions
    let result = vm.call_function("length", &[Value::array(&[1, 2, 3, 4, 5])])?;
    assert_eq!(result.as_i32()?, 5);

    // Type checking
    let func = vm.get_global("square").unwrap();
    assert!(func.is_function());

    // Automatic conversions from Rust types
    let values: Vec<Value> = vec![
        42.into(),      // i32
        3.14.into(),    // f64
        "hello".into(), // &str
        true.into(),    // bool
    ];

    // Ergonomic collection creation
    let numbers = Value::array(&[1, 2, 3, 4, 5]);     // No Value::number() required!
    let mixed = janet_array![1, "hello", true, 3.14]; // Macro for mixed types

    // Direct array/vec conversion
    let from_vec: Value = vec![10, 20, 30].into();
    let from_array: Value = [1, 2, 3].into();

    // Ergonomic table operations
    let mut table = Table::new();
    table.set("name", "Janet");   // No Value::string() needed
    table.set("version", 139);    // No Value::number() needed
    table.set(42, "answer");      // Mixed key types work

    // Work with collections
    let result = vm.run("@[1 2 3 4 5]")?;
    let array = result.as_array()?;
    println!("Array length: {}", array.len());

    for (i, value) in array.iter().enumerate() {
        println!("[{}] = {} (as i32: {})", i, value, value.as_i32()?);
    }

    // Symbols and keywords
    let symbol = Symbol::new("test-symbol");
    let keyword = Keyword::new("test-keyword");

    let mut table = Table::new();
    table.set(symbol, "symbol value");
    table.set(keyword, "keyword value");

    // Janet-created symbols/keywords
    let janet_symbol = vm.run("'my-symbol")?;
    let janet_keyword = vm.run(":my-keyword")?;

    println!("Symbol: {}", janet_symbol.as_symbol()?);
    println!("Keyword: {}", janet_keyword.as_keyword()?);

    Ok(())
}

Ergonomic API

The bindings prioritize ergonomics and idiomatic Rust:

Automatic Conversions

// No need for explicit Value::number(), Value::string(), etc.
let val: Value = 42.into();        // i32 -> Value
let val: Value = "hello".into();   // &str -> Value
let val: Value = vec![1,2,3].into(); // Vec<i32> -> Value (array)

// Generic constructors accept any convertible type
let num = Value::number(42);       // Works with i32, i64, f32, f64
let arr = Value::array(&[1, 2, 3]); // Works with any Into<Value>

Collection Macros

let array = janet_array![1, "hello", true, 3.14];
let tuple = janet_tuple!["name", "value", 42];

Ergonomic Methods

let mut arr = Array::new();
arr.push(42);        // No Value::number() wrapping
arr.push("test");    // No Value::string() wrapping

let mut table = Table::new();
table.set("key", 123);  // Automatic conversions
table.set(42, "value"); // Mixed types work naturally

Function Calling

// Define Janet function
vm.run("(defn greet [name] (string \"Hello, \" name \"!\"))")?;

// Call it from Rust
let result = vm.call_function("greet", &["World".into()])?;
println!("{}", result.as_str()?); // "Hello, World!"

// Or get the function value first
let greet_fn = vm.get_global("greet").unwrap();
let result = greet_fn.call(&["Rust".into()])?;

Registering Rust Functions

use janet_binds::{janet_fn, janet_fns, VM, Value, Result};

// Define a Rust function
fn rust_add(args: &[Value]) -> Result<Value> {
    let a = args.get(0).and_then(|v| v.as_i32().ok()).unwrap_or(0);
    let b = args.get(1).and_then(|v| v.as_i32().ok()).unwrap_or(0);
    Ok((a + b).into())
}

let vm = VM::new()?;

// Register individual function
janet_fn!(vm, "rust-add", rust_add);

// Call from Janet
let result = vm.run("(rust-add 10 32)")?;
assert_eq!(result.as_i32()?, 42);

// Register multiple functions with prefix
fn fs_read(args: &[Value]) -> Result<Value> { /* ... */ }
fn fs_write(args: &[Value]) -> Result<Value> { /* ... */ }

janet_fns!(vm, "fs", {
    "read" => fs_read,
    "write" => fs_write,
});

// Janet sees fs/read and fs/write
vm.run("(fs/read \"/etc/os-release\")")?;

Building

This crate bundles Janet 1.39.1 as an amalgamated C source and compiles it automatically.

cargo build
cargo run --example showcase     # Comprehensive API demonstration
cargo run --example functions    # Calling Janet from Rust
cargo run --example cfunctions   # Registering Rust functions in Janet

License

This library is licensed under MIT license.