Skip to content

Native Plugins & Advanced FFI

In addition to external crates, you can write custom native Rust code inside your project and call it directly from Flame with zero runtime overhead.


The easiest way to initialize a native plugin in your Flame project is via the Flame CLI:

Terminal window
fmp new --plugin <plugin_name>
# or shorthand:
fmp new -p <plugin_name>

This command will automatically scaffold a Rust library in your ./native folder and register it in your flame.toml. If you omit the plugin name, the CLI defaults to naming the plugin bridge.

If you have an existing plugin or modular Rust crate, register it into your project with:

Terminal window
fmp add --plugin ./native
# or shorthand:
fmp add -p ./native

Flame automatically extracts the plugin name directly from the plugin’s Cargo.toml ([package] name = "..."), so passing a separate --name flag is not required.

Flame’s FFI boundary natively supports a vast majority of standard Rust elements simply by marking them pub. There are no boilerplate binding requirements—just write normal Rust code and use it directly via import native.<plugin_name>.

Supported Native Constructs

  • pub struct declarations
  • impl blocks and methods
  • Module-level pub fn functions
  • All standard primitive types (u8, i32, f64, String, etc.)
  • Option<T> and Result<T, E> types
  • Rust Generics (e.g., <T>)

Currently Unsupported

  • Complex wrapper types directly in parameters or returns (e.g., Mutex<T>, Box<T>, Arc<T>)
  • Raw pointers or unsafe memory boundaries

Building a Custom Plugin: Axum + Tokio Web Server

Section titled “Building a Custom Plugin: Axum + Tokio Web Server”
  1. Create a native folder containing native/Cargo.toml and native/src/lib.rs:

    native/Cargo.toml
    [package]
    name = "server"
    version = "0.1.0"
    edition = "2021"
    [dependencies]
    axum = "0.7"
    tokio = { version = "1.0", features = ["full"] }
    flame-macro = "0.1.0"
  2. native/src/lib.rs
    use axum::{
    routing::{get, post},
    Router,
    };
    use flame_macro::flame;
    use std::mem;
    use std::net::SocketAddr;
    pub struct FlameServer {
    router: Router,
    }
    #[derive(Debug, Clone)]
    pub struct Request {
    pub body: String,
    }
    #[derive(Debug, Clone)]
    pub struct Response {
    pub body: String,
    }
    // Module-level function
    /// Initialize a new FlameServer instance.
    pub fn init() -> FlameServer {
    FlameServer {
    router: Router::new(),
    }
    }
    impl FlameServer {
    pub fn get<H, T>(&mut self, path: &'static str, handler: H)
    where
    H: axum::handler::Handler<T, ()> + Clone + Send + Sync + 'static,
    T: 'static,
    {
    let router = mem::take(&mut self.router);
    self.router = router.route(path, get(handler));
    }
    pub fn post<H, T>(&mut self, path: &'static str, handler: H)
    where
    H: axum::handler::Handler<T, ()> + Clone + Send + Sync + 'static,
    T: 'static,
    {
    let router = mem::take(&mut self.router);
    self.router = router.route(path, post(handler));
    }
    pub fn router(&mut self) -> Router {
    mem::take(&mut self.router)
    }
    #[flame(daemon)]
    pub async fn listen(self, port: u16) -> std::io::Result<()> {
    let addr = SocketAddr::from(([127, 0, 0, 1], port));
    let listener = tokio::net::TcpListener::bind(addr).await?;
    axum::serve(listener, self.router)
    .await
    .map_err(std::io::Error::other)
    }
    }
  3. [plugins]
    server = "./native"
  4. import native.server
    let app = server.init()
    fn main() -> String {
    "hello"
    }
    app.get("/", main)
    print("Server started!")
    app.listen(3000)

Selective Inclusion in the Application-Specific Runtime

Section titled “Selective Inclusion in the Application-Specific Runtime”

Native Rust implementations are incorporated into the executable only when required by the application’s dependency graph:

Application
│
├── Flame source
├── flame.toml
└── package .fm + .fmi
↓
Dependency Analysis
↓
Required native plugins
↓
Application Runtime
↓
Cargo + LLVM
↓
Native Binary
  1. No Manual Source Copying: The application developer does not manually copy Rust plugin source code, Cargo projects, DLL project trees, or native implementation folders into their application.
  2. Clean Interface Boundary (.fmi): The application relies on the Flame-side package source (.fm) and the .fmi interface metadata.
  3. Dead Code Elimination: If a native plugin is registered in flame.toml but not actually imported in the code, Flame’s dependency analysis excludes it from the application’s specialized runtime.
  4. Production Hygiene: Because the specialized runtime binary compiled by flame build --release contains all native plugin code, production runner containers or servers do not need Rust, Cargo, or any .rs files.