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 | shAdd 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.