🚧 Glyx is pre-release software. APIs may change before v1.0. Get started →
Documentation
Guides
Native Extensions

Native Extensions Experimental

⚠️

Experimental — the native extension API may change before v1.0.

Native extensions let you write Rust code that your JavaScript can call directly. Use them for performance-critical work, C library bindings, or OS APIs not exposed by Glyx.

If you only need app-level backend logic and want to stay in JavaScript, start with JS Plugins.

When to use native extensions

Use a native extension when you need to:

  • Process large files without blocking the JS thread
  • Bind to a C/C++ library (image processing, cryptography, etc.)
  • Access OS APIs not yet covered by Glyx's built-in bindings
  • Run CPU-intensive loops at native speed

For most apps, the built-in bindings cover everything needed.

Setup

You need Rust installed for native extensions (unlike the rest of Glyx):

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Add a new crate to your workspace and depend on glyx-core:

# extensions/my-extension/Cargo.toml
[package]
name = "my-extension"
version = "0.1.0"
edition = "2021"
 
[lib]
crate-type = ["cdylib"]
 
[dependencies]
glyx-core = { path = "../../crates/glyx-core" }
sha2 = "0.10"

Write the extension

Implement the GlyxExtension trait and register your commands with BackendRegistryBuilder:

// extensions/my-extension/src/lib.rs
use glyx_core::extension::{GlyxExtension, BackendRegistryBuilder, ExtensionContext};
 
pub struct MyExtension;
 
impl GlyxExtension for MyExtension {
    fn name(&self) -> &'static str { "my-extension" }
 
    fn register(&self, builder: &mut BackendRegistryBuilder) {
        builder.command("hash_file", |ctx: ExtensionContext| {
            let path: String = ctx.arg(0)?;
            let hash = hash_file_impl(&path)?;
            ctx.resolve(hash)
        });
 
        builder.command_async("compress_file", |ctx: ExtensionContext| async move {
            let input: String = ctx.arg(0)?;
            let output: String = ctx.arg(1)?;
            let bytes_written = compress_impl(&input, &output).await?;
            ctx.resolve(bytes_written)
        });
    }
}
 
fn hash_file_impl(path: &str) -> anyhow::Result<String> {
    use std::io::Read;
    use sha2::{Sha256, Digest};
 
    let mut file = std::fs::File::open(path)?;
    let mut hasher = Sha256::new();
    let mut buf = [0u8; 8192];
 
    loop {
        let n = file.read(&mut buf)?;
        if n == 0 { break; }
        hasher.update(&buf[..n]);
    }
 
    Ok(format!("{:x}", hasher.finalize()))
}

Register the extension in your app's main crate:

// src/main.rs
use glyx_core::GlyxApp;
use my_extension::MyExtension;
 
fn main() {
    GlyxApp::new()
        .extension(MyExtension)
        .run();
}

Call from JavaScript

Registered commands are available on the backend object injected into JS:

// Synchronous command
const hash = backend.hashFile('/path/to/file.txt')
 
// Async command (runs on background thread, non-blocking)
const bytes = await backend.compressFile('/path/to/large.csv', '/path/to/large.csv.gz')

Command names are camelCased from their snake_case Rust registration name.

TypeScript wrapper (optional)

Wrap the raw backend calls in typed functions:

// extensions/my-extension/index.ts
export const hashFile = (path: string): string =>
  backend.hashFile(path)
 
export const compressFile = (input: string, output: string): Promise<number> =>
  backend.compressFile(input, output)

The backend global is injected at runtime. TypeScript won't know about it unless you declare it: declare const backend: Record<string, (...args: unknown[]) => unknown>.

Build

Extensions compile as part of the normal glyx build / glyx dev cycle since they live in the Rust workspace. No separate build step is needed.