Records

Records are value types: structs and enums marked with #[data]. When a record crosses the boundary, it gets copied. The target language receives its own copy of the data.

Classes are different. A class lives in Rust, and the target language holds a reference to it. See Classes for when to use one over the other.

Structs

Mark a struct with #[data] and it becomes a value type that can cross the FFI boundary. The caller creates an instance, passes it in, and BoltFFI handles the transfer.

The cost of passing a struct depends entirely on its contents. A struct with only primitive fields is nearly as fast as passing primitives directly. The bytes pack together and move across the boundary with no per-field overhead. This is the zero-copy path that makes BoltFFI fast.

A struct containing strings or collections takes longer to move across the boundary. Each string means an allocation on the receiving side. Each collection means transferring its length and all elements. Still fast in absolute terms, but noticeably slower than primitive-only structs in tight loops.

Primitive-only structs

The bytes pack together and move across the boundary with no per-field overhead. This is the fast path.

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

#[data]
pub struct Rect {
  pub x: f64,
  pub y: f64,
  pub width: f64,
  pub height: f64,
}

#[export]
pub fn rect_contains(
  rect: Rect,
  point: Point
) -> bool {
  point.x >= rect.x
      && point.x <= rect.x + rect.width
      && point.y >= rect.y
      && point.y <= rect.y + rect.height
}
public struct Point {
  public var x: Double
  public var y: Double
}

public struct Rect {
  public var x: Double
  public var y: Double
  public var width: Double
  public var height: Double
}

func rectContains(rect: Rect, point: Point) -> Bool

let r = Rect(x: 0, y: 0, width: 100, height: 100)
let p = Point(x: 50, y: 50)
let inside = rectContains(rect: r, point: p)

Structs with strings or collections

These take longer to cross the boundary. A struct with two strings means two allocations on the receiving side. Still fast for normal use, but measure if you’re calling thousands of times per second.

RustSource
#[data]
pub struct User {
  pub id: u64,
  pub name: String,
  pub email: String,
}

#[data]
pub struct SearchResult {
  pub query: String,
  pub matches: Vec<User>,
  pub total_count: u64,
}

#[export]
pub fn search_users(
  query: &str
) -> SearchResult {
  // ... search logic
  SearchResult {
      query: query.to_string(),
      matches: vec![],
      total_count: 0,
  }
}
public struct User {
  public var id: UInt64
  public var name: String
  public var email: String
}

public struct SearchResult {
  public var query: String
  public var matches: [User]
  public var totalCount: UInt64
}

func searchUsers(query: String) -> SearchResult

let result = searchUsers(query: "alice")

Nested structs

A struct can contain other structs, and BoltFFI handles them recursively. There’s no depth limit imposed by BoltFFI, though deeply nested structures take more time to move across the boundary.

RustSource
#[data]
pub struct Address {
  pub street: String,
  pub city: String,
  pub zip: String,
}

#[data]
pub struct Company {
  pub name: String,
  pub address: Address,
  pub employee_count: u32,
}

#[export]
pub fn company_summary(c: Company) -> String {
  format!(
      "{} in {}, {} employees",
      c.name, c.address.city, c.employee_count
  )
}
public struct Address {
  public var street: String
  public var city: String
  public var zip: String
}

public struct Company {
  public var name: String
  public var address: Address
  public var employeeCount: UInt32
}

func companySummary(c: Company) -> String

let addr = Address(
  street: "123 Main",
  city: "Seattle",
  zip: "98101"
)
let co = Company(
  name: "Acme",
  address: addr,
  employeeCount: 50
)
let summary = companySummary(c: co)

Optional fields

Option<T> in Rust becomes a nullable type in the target language. None becomes null or nil on the other side.

RustSource
#[data]
pub struct Profile {
  pub username: String,
  pub display_name: Option<String>,
  pub bio: Option<String>,
  pub follower_count: u64,
}

#[export]
pub fn display_label(p: Profile) -> String {
  p.display_name
      .unwrap_or(p.username)
}
public struct Profile {
  public var username: String
  public var displayName: String?
  public var bio: String?
  public var followerCount: UInt64
}

func displayLabel(p: Profile) -> String

let p = Profile(
  username: "alice",
  displayName: "Alice Smith",
  bio: nil,
  followerCount: 1000
)
let label = displayLabel(p: p)

Default values

Use #[boltffi::default(...)] on a field to give it a default value. The generated constructor will have that parameter as optional.

RustSource
#[data]
pub struct Config {
  pub name: String,
  #[boltffi::default(3)]
  pub retries: i32,
  #[boltffi::default(true)]
  pub enabled: bool,
  #[boltffi::default("localhost")]
  pub host: String,
}
public struct Config {
  public var name: String
  public var retries: Int32
  public var enabled: Bool
  public var host: String
  
  public init(
      name: String,
      retries: Int32 = 3,
      enabled: Bool = true,
      host: String = "localhost"
  )
}

let cfg = Config(name: "myapp")

Supported default values

ValueExample
Booleans#[boltffi::default(true)]
Integers#[boltffi::default(42)]
Floats#[boltffi::default(2.5)]
Strings#[boltffi::default("hello")]
Enum variants#[boltffi::default(Status::Pending)]
None#[boltffi::default(None)]

Option fields automatically default to nil/null if no explicit default is provided.

Enums

Enums in Rust are more powerful than enums in most languages. A Rust enum can have variants with no data, variants with different data types, or any combination. BoltFFI handles all of these cases, generating the appropriate type in each target language.

Simple enums

Enums with no associated data become native enum types that you can switch on, compare, or pass around.

RustSource
#[data]
pub enum Status {
  Pending,
  Active,
  Completed,
  Failed,
}

#[export]
pub fn status_label(s: Status) -> String {
  match s {
      Status::Pending => "pending",
      Status::Active => "active",
      Status::Completed => "done",
      Status::Failed => "failed",
  }.to_string()
}
public enum Status {
  case pending
  case active
  case completed
  case failed
}

func statusLabel(s: Status) -> String

let label = statusLabel(s: .active)

Enums with associated data

Each variant can carry different data. A loading state might carry progress; an error state might carry a message and code.

RustSource
#[data]
pub enum LoadState {
  Idle,
  Loading { progress: f64 },
  Loaded { data: String },
  Error { message: String, code: i32 },
}

#[export]
pub fn describe_state(s: LoadState) -> String {
  match s {
      LoadState::Idle => 
          "Not started".to_string(),
      LoadState::Loading { progress } => 
          format!("{}%", (progress * 100.0) as i32),
      LoadState::Loaded { data } => 
          format!("Done: {} bytes", data.len()),
      LoadState::Error { message, .. } => 
          format!("Failed: {}", message),
  }
}
public enum LoadState {
  case idle
  case loading(progress: Double)
  case loaded(data: String)
  case error(message: String, code: Int32)
}

func describeState(s: LoadState) -> String

let s = LoadState.loading(progress: 0.5)
let desc = describeState(s: s)