How to Create and Load Custom Extensions in Turso
Turso supports SQLite-compatible extensions written in Rust that you can define using the turso_ext macro system, compile as shared libraries, and dynamically load into active database connections.
Creating and loading custom extensions in Turso allows you to extend SQL functionality with scalar functions, aggregates, virtual tables, or VFS modules written in Rust. The extension system mirrors SQLite's native extension mechanism while providing a type-safe, Rust-centric API. This guide walks through the complete lifecycle from writing your first extension in tursodatabase/turso to loading it via SQL, the Rust SDK, or the CLI.
Creating a Custom Extension
The extension lifecycle begins with defining a Rust crate that exports a C-ABI entry point using Turso's macro system.
Writing the Rust Entry Point
Your extension must export a register_extension function that Turso's loader can discover and invoke. The turso_ext crate provides the register_extension! macro to generate this boilerplate automatically.
In macros/src/ext/mod.rs, the macro generates a C-ABI function named register_extension (or register_extension_static for static linking) that receives a pointer to Turso's extension API. Inside the macro, you declare the functions, aggregates, or virtual tables you want to expose to SQL.
Each exported function is a standard Rust fn that receives &[Value] and returns a Value. Here is the structure from the built-in regexp extension at extensions/regexp/src/lib.rs:
use turso_ext::{register_extension, scalar, Value, ValueType};
register_extension! {
scalars: { regexp, regexp_like, regexp_substr, regexp_replace, regexp_capture }
}
#[scalar(name = "regexp")]
fn regexp(args: &[Value]) -> Value {
// Implementation receives arguments and returns a Value
}
The macro expands to a C-ABI function that initializes your extension when the library is loaded.
Compiling the Shared Library
To create a loadable extension, configure your Cargo.toml to build a dynamic library:
[lib]
name = "my_extension"
crate-type = ["cdylib"]
Build with cargo build --release. On Linux and macOS, this produces libmy_extension.so; on Windows, my_extension.dll. The resulting file must contain the register_extension symbol that Turso's loader resolves at runtime.
Loading Extensions at Runtime
Turso loads extensions dynamically through a secure mechanism that manages library lifetimes and schema updates.
Enabling Extension Loading
By default, extension loading is disabled for security. You must explicitly enable it before calling load_extension.
In the Rust SDK (sdk-kit/src/rsapi.rs), toggle the flag:
conn.set_load_extension_enabled(true);
In SQL, extensions load only if the flag is already enabled:
SELECT load_extension('my_extension.so');
The CLI REPL implements enabling in cli/app.rs via the handle_load_extension method.
The Loading Mechanism
The core implementation resides in core/ext/dynamic.rs. The Connection::load_extension method (lines 39-77) performs these steps:
- Builds a Turso extension API struct for the connection.
- Dynamically loads the
.sofile vialibloading::Library. - Resolves the
register_extensionsymbol. - Invokes the entry point, passing a pointer to the API.
- Stores the library in a static
OnceLock<Arc<Mutex<Vec<(Arc<Library>, ExtensionApiRef)>>>>to prevent unloading and keep symbols valid for the process lifetime. - Re-parses the database schema so new functions become immediately visible to SQL queries.
This mechanism ensures that extension functions persist as long as the connection remains open without leaking memory or allowing unsafe library unloading.
Practical Examples
Minimal Scalar Function Extension
Here is a complete custom extension that exposes a my_len function returning the character count of a text value:
// src/lib.rs
use turso_ext::{register_extension, scalar, Value, ValueType};
register_extension! { scalars: { my_len } }
#[scalar(name = "my_len")]
fn my_len(args: &[Value]) -> Value {
match args.get(0) {
Some(v) if v.value_type() == ValueType::Text => {
let txt = v.to_text().unwrap_or_default();
Value::from_integer(txt.chars().count() as i64)
}
_ => Value::null(),
}
}
With Cargo.toml configured as a cdylib, build and locate target/release/libmy_len.so.
Loading from the Rust SDK
Use the SDK to programmatically load your extension:
use turso_sdk::Connection;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let conn = Connection::open("file:my.db?mode=memory")?;
conn.set_load_extension_enabled(true);
conn.load_extension("target/release/libmy_len.so")?;
// The function is now available
let rows = conn.execute("SELECT my_len('turso')")?;
println!("{:?}", rows); // Returns 5
Ok(())
}
The underlying call routes to Connection::load_extension in core/ext/dynamic.rs, which handles the dynamic linking and registration.
Loading from the CLI
In the tursodb REPL, use the dot command:
$ tursodb repl
> .load_extension target/release/libmy_len.so
Loaded extension from 'target/release/libmy_len.so'
> SELECT my_len('database');
9
The REPL invokes handle_load_extension at cli/app.rs (lines 374-381) to process the command and confirm successful loading.
Summary
- Define extensions using the
turso_extmacro system in Rust, which generates the requiredregister_extensionC-ABI entry point. - Compile as
cdylibto produce.so(Linux/macOS) or.dll(Windows) files containing the registration symbol. - Enable loading via
set_load_extension_enabled(true)before callingload_extensionon the connection. - Load dynamically through
core/ext/dynamic.rs, which manages library lifetimes in a staticOnceLockstore and re-parses the schema to expose new functions. - Access via SQL, Rust SDK, or CLI REPL once loaded, with functions immediately available to queries.
Frequently Asked Questions
What file format must Turso extensions use?
Turso extensions must be compiled as dynamic shared libraries. On Linux and macOS, use .so files; on Windows, use .dll files. Configure your Cargo.toml with crate-type = ["cdylib"] to generate the correct format.
Can I unload an extension after loading it?
No. Turso intentionally prevents unloading extensions to ensure memory safety. The loader stores each library in a static EXTENSIONS vector using OnceLock and Arc to keep the library alive for the process lifetime. This guarantees that function pointers and virtual table modules remain valid.
Why does my extension function not appear in SQL immediately?
You must enable extension loading before attempting to load. Call set_load_extension_enabled(true) on the connection or use the equivalent SQL pragma. The load_extension function in core/ext/dynamic.rs automatically re-parses the schema after successful registration, so functions become visible immediately after the load call completes successfully.
Does Turso support SQLite extensions written in C?
Turso supports SQLite-compatible extensions written in Rust using the turso_ext macro system. While the extension system is designed to be compatible with SQLite's extension mechanism, the recommended approach is writing Rust code that uses the Value type and register_extension! macro for type safety and integration with Turso's specific API.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →