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:

  1. Builds a Turso extension API struct for the connection.
  2. Dynamically loads the .so file via libloading::Library.
  3. Resolves the register_extension symbol.
  4. Invokes the entry point, passing a pointer to the API.
  5. Stores the library in a static OnceLock<Arc<Mutex<Vec<(Arc<Library>, ExtensionApiRef)>>>> to prevent unloading and keep symbols valid for the process lifetime.
  6. 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_ext macro system in Rust, which generates the required register_extension C-ABI entry point.
  • Compile as cdylib to produce .so (Linux/macOS) or .dll (Windows) files containing the registration symbol.
  • Enable loading via set_load_extension_enabled(true) before calling load_extension on the connection.
  • Load dynamically through core/ext/dynamic.rs, which manages library lifetimes in a static OnceLock store 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →