Functions

This page covers free functions - standalone functions that aren’t attached to a struct or class. For methods on objects, see Classes.

Mark a Rust function with #[export] and BoltFFI generates a corresponding function in each target language. The function signature, parameter types, and return type all map according to the rules in Types.

Function names may be renamed to match target language conventions. For example, get_user becomes getUser in languages that use camelCase.

Basic export

The simplest case: a function that takes primitives and returns a primitive.

RustSource
#[export]
pub fn add(a: i32, b: i32) -> i32 {
  a + b
}

#[export]
pub fn multiply(x: f64, y: f64) -> f64 {
  x * y
}
func add(a: Int32, b: Int32) -> Int32
func multiply(x: Double, y: Double) -> Double

let sum = add(a: 5, b: 3)
let product = multiply(x: 2.5, y: 4.0)

Parameters

Primitives and strings

Primitive parameters pass directly with no overhead. Strings require copying since each language manages its own memory.

RustSource
#[export]
pub fn greet(name: &str) -> String {
  format!("Hello, {}!", name)
}

#[export]
pub fn char_count(s: &str) -> usize {
  s.chars().count()
}
func greet(name: String) -> String
func charCount(s: String) -> UInt

let msg = greet(name: "World")
let len = charCount(s: "hello")

Use &str for string parameters (you’re borrowing) and String for return values (you’re transferring ownership).

Structs and enums

Functions can accept structs and enums marked with #[data]. The data and all its fields move across the boundary.

RustSource
#[data]
pub struct Point {
  pub x: f64,
  pub y: f64,
}

#[data]
pub enum Unit {
  Meters,
  Feet,
}

#[export]
pub fn distance(a: Point, b: Point, unit: Unit) -> f64 {
  let dx = b.x - a.x;
  let dy = b.y - a.y;
  let d = (dx*dx + dy*dy).sqrt();
  match unit {
      Unit::Meters => d,
      Unit::Feet => d * 3.28084,
  }
}
func distance(a: Point, b: Point, unit: Unit) -> Double

let d = distance(
  a: Point(x: 0, y: 0),
  b: Point(x: 3, y: 4),
  unit: .meters
)

Slices

Use &[T] to accept a collection without taking ownership. The caller’s array or list is accessible as a slice inside Rust.

RustSource
#[export]
pub fn sum(values: &[i32]) -> i32 {
  values.iter().sum()
}

#[export]
pub fn average(values: &[f64]) -> f64 {
  if values.is_empty() {
      return 0.0;
  }
  values.iter().sum::<f64>() / values.len() as f64
}
func sum(values: [Int32]) -> Int32
func average(values: [Double]) -> Double

let total = sum(values: [1, 2, 3, 4, 5])
let avg = average(values: [1.0, 2.0, 3.0])

Optional

Use Option<T> when a parameter might not be provided. The caller passes nil/null or a value.

RustSource
#[export]
pub fn greet_user(
  name: &str,
  title: Option<String>
) -> String {
  match title {
      Some(t) => format!("Hello, {} {}!", t, name),
      None => format!("Hello, {}!", name),
  }
}

#[export]
pub fn set_timeout(
  ms: u64,
  callback_id: Option<u64>
) {
  // ...
}
func greetUser(
  name: String,
  title: String?
) -> String

func setTimeout(
  ms: UInt64,
  callbackId: UInt64?
)

let formal = greetUser(name: "Smith", title: "Dr.")
let casual = greetUser(name: "John", title: nil)

Return types

Option

Return Option<T> when a value might not exist. The caller gets a nullable type they can check before using.

RustSource
#[export]
pub fn find_user(id: u64) -> Option<User> {
  database.get(&id).cloned()
}

#[export]
pub fn first_positive(values: &[i32]) -> Option<i32> {
  values.iter().copied().find(|&x| x > 0)
}
func findUser(id: UInt64) -> User?
func firstPositive(values: [Int32]) -> Int32?

if let user = findUser(id: 42) {
  print(user.name)
}

let pos = firstPositive(values: [-1, -2, 3])

Result

Return Result<T, E> when an operation can fail. The error type must be marked with #[error]. The generated function throws in the target language.

RustSource
#[error]
pub enum ParseError {
  InvalidFormat,
  OutOfRange,
}

#[export]
pub fn parse_port(s: &str) -> Result<u16, ParseError> {
  let n: u32 = s.parse()
      .map_err(|_| ParseError::InvalidFormat)?;
  if n > 65535 {
      return Err(ParseError::OutOfRange);
  }
  Ok(n as u16)
}
func parsePort(s: String) throws -> UInt16

do {
  let port = try parsePort(s: "8080")
} catch ParseError.invalidFormat {
  print("bad format")
} catch ParseError.outOfRange {
  print("too large")
}

Use Option when absence is expected. Use Result when absence is an error.

Vec

Return Vec<T> when you’re producing a collection. Each element moves across the boundary.

RustSource
#[export]
pub fn range(start: i32, end: i32) -> Vec<i32> {
  (start..end).collect()
}

#[export]
pub fn filter_positive(values: &[i32]) -> Vec<i32> {
  values.iter().copied().filter(|&x| x > 0).collect()
}
func range(start: Int32, end: Int32) -> [Int32]
func filterPositive(values: [Int32]) -> [Int32]

let nums = range(start: 0, end: 10)
let pos = filterPositive(values: [-1, 2, -3, 4])

Async functions

Mark a function async and BoltFFI generates an async function in the target language. Swift gets async, Kotlin gets suspend.

RustSource
#[export]
pub async fn fetch_data(url: &str) -> Result<String, FetchError> {
  let response = client.get(url).await?;
  let body = response.text().await?;
  Ok(body)
}

#[export]
pub async fn load_config() -> Config {
  let bytes = read_file("config.json").await;
  parse_config(&bytes)
}
func fetchData(url: String) async throws -> String
func loadConfig() async -> Config

let data = try await fetchData(url: "https://api.example.com")
let config = await loadConfig()

The async runtime is handled by BoltFFI. Your Rust async code runs on a Tokio runtime that BoltFFI manages.

Closures

Functions can accept closures as parameters. The closure is called synchronously within the Rust function.

RustSource
#[export]
pub fn foreach_range(
  start: i32,
  end: i32,
  mut callback: impl FnMut(i32)
) {
  (start..end).for_each(|i| callback(i));
}

#[export]
pub fn map_values(
  values: &[i32],
  transform: impl Fn(i32) -> i32
) -> Vec<i32> {
  values.iter().map(|&x| transform(x)).collect()
}
func foreachRange(
  start: Int32,
  end: Int32,
  callback: (Int32) -> Void
)

func mapValues(
  values: [Int32],
  transform: (Int32) -> Int32
) -> [Int32]

var sum: Int32 = 0
foreachRange(start: 1, end: 5) { sum += $0 }

let doubled = mapValues(values: [1, 2, 3]) { $0 * 2 }

Each closure call crosses the FFI boundary. If you’re calling the closure many times in a tight loop, consider restructuring to reduce crossings.

Limitations

  • Generic functions like fn max<T: Ord>(a: T, b: T) -> T are not supported. Create concrete versions for each type you need.

  • Functions cannot return references. Return owned data instead.

  • Closures that outlive the function call (stored for later use) are not supported. The closure must be called within the function body.

// Not supported - generic
#[export]
pub fn max<T: Ord>(a: T, b: T) -> T { ... }

// Supported - concrete versions
#[export]
pub fn max_i32(a: i32, b: i32) -> i32 {
    if a > b { a } else { b }
}

#[export]
pub fn max_f64(a: f64, b: f64) -> f64 {
    if a > b { a } else { b }
}