Skip to content

Commit 55e79a7

Browse files
committed
Merge 'Initial pass on loadable rust extensions' from Preston Thorpe
This PR adds the start of an implementation of an extension library for `limbo` so users can write extensions in rust, that can be loaded at runtime with the `.load` cli command. The existing "ExtensionFunc" `uuid`, has been replaced with the first complete limbo extension in `extensions/uuid` ![image](https://github.com/user- attachments/assets/63aa06cc-9390-4277-ba09-3a9be5524535) ![image](https://github.com/user- attachments/assets/75899748-5e26-406a-84ee-26063383afeb) There is still considerable work to do on this, as this only implements scalar functions, but this PR is already plenty big enough. Design + implementation comments or suggestions would be appreciated :+1: I tried out using `abi_stable`, so that trait objects and other goodies could be used across FFI bounds, but to be honest I didn't find it too much better than this. I personally haven't done a whole lot with FFI, or anything at all linking dynamically in Rust, so if there is something I seem to be missing here, please let me know. I added some tests, similar to how shell-tests are setup. If anyone can test this on other platforms, that would be helpful as well as I am limited to x86_64 linux here Closes #658
2 parents bfbaa80 + 9c208dc commit 55e79a7

File tree

21 files changed

+917
-511
lines changed

21 files changed

+917
-511
lines changed

Cargo.lock

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ members = [
1111
"sqlite3",
1212
"core",
1313
"simulator",
14-
"test", "macros",
14+
"test", "macros", "limbo_extension", "extensions/uuid",
1515
]
1616
exclude = ["perf/latency/limbo"]
1717

Makefile

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,15 @@ limbo-wasm:
6262
cargo build --package limbo-wasm --target wasm32-wasi
6363
.PHONY: limbo-wasm
6464

65-
test: limbo test-compat test-sqlite3 test-shell
65+
test: limbo test-compat test-sqlite3 test-shell test-extensions
6666
.PHONY: test
6767

68-
test-shell: limbo
68+
test-extensions: limbo
69+
cargo build --package limbo_uuid
70+
./testing/extensions.py
71+
.PHONY: test-extensions
72+
73+
test-shell: limbo
6974
SQLITE_EXEC=$(SQLITE_EXEC) ./testing/shelltests.py
7075
.PHONY: test-shell
7176

cli/app.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ pub enum Command {
129129
Tables,
130130
/// Import data from FILE into TABLE
131131
Import,
132+
/// Loads an extension library
133+
LoadExtension,
132134
}
133135

134136
impl Command {
@@ -141,7 +143,12 @@ impl Command {
141143
| Self::ShowInfo
142144
| Self::Tables
143145
| Self::SetOutput => 0,
144-
Self::Open | Self::OutputMode | Self::Cwd | Self::Echo | Self::NullValue => 1,
146+
Self::Open
147+
| Self::OutputMode
148+
| Self::Cwd
149+
| Self::Echo
150+
| Self::NullValue
151+
| Self::LoadExtension => 1,
145152
Self::Import => 2,
146153
} + 1) // argv0
147154
}
@@ -160,6 +167,7 @@ impl Command {
160167
Self::NullValue => ".nullvalue <string>",
161168
Self::Echo => ".echo on|off",
162169
Self::Tables => ".tables",
170+
Self::LoadExtension => ".load",
163171
Self::Import => &IMPORT_HELP,
164172
}
165173
}
@@ -182,6 +190,7 @@ impl FromStr for Command {
182190
".nullvalue" => Ok(Self::NullValue),
183191
".echo" => Ok(Self::Echo),
184192
".import" => Ok(Self::Import),
193+
".load" => Ok(Self::LoadExtension),
185194
_ => Err("Unknown command".to_string()),
186195
}
187196
}
@@ -314,6 +323,11 @@ impl Limbo {
314323
};
315324
}
316325

326+
#[cfg(not(target_family = "wasm"))]
327+
fn handle_load_extension(&mut self, path: &str) -> Result<(), String> {
328+
self.conn.load_extension(path).map_err(|e| e.to_string())
329+
}
330+
317331
fn display_in_memory(&mut self) -> std::io::Result<()> {
318332
if self.opts.db_file == ":memory:" {
319333
self.writeln("Connected to a transient in-memory database.")?;
@@ -537,6 +551,13 @@ impl Limbo {
537551
let _ = self.writeln(e.to_string());
538552
};
539553
}
554+
Command::LoadExtension =>
555+
{
556+
#[cfg(not(target_family = "wasm"))]
557+
if let Err(e) = self.handle_load_extension(args[1]) {
558+
let _ = self.writeln(&e);
559+
}
560+
}
540561
}
541562
} else {
542563
let _ = self.write_fmt(format_args!(

core/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ rustix = "0.38.34"
3535
mimalloc = { version = "*", default-features = false }
3636

3737
[dependencies]
38+
limbo_extension = { path = "../limbo_extension" }
3839
cfg_block = "0.1.1"
3940
fallible-iterator = "0.3.0"
4041
hex = "0.4.3"
@@ -58,6 +59,7 @@ bumpalo = { version = "3.16.0", features = ["collections", "boxed"] }
5859
limbo_macros = { path = "../macros" }
5960
uuid = { version = "1.11.0", features = ["v4", "v7"], optional = true }
6061
miette = "7.4.0"
62+
libloading = "0.8.6"
6163

6264
[target.'cfg(not(target_family = "windows"))'.dev-dependencies]
6365
pprof = { version = "0.14.0", features = ["criterion", "flamegraph"] }

core/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ pub enum LimboError {
3939
InvalidModifier(String),
4040
#[error("Runtime error: {0}")]
4141
Constraint(String),
42+
#[error("Extension error: {0}")]
43+
ExtensionError(String),
4244
}
4345

4446
#[macro_export]

core/ext/mod.rs

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,44 @@
1-
#[cfg(feature = "uuid")]
2-
mod uuid;
3-
#[cfg(feature = "uuid")]
4-
pub use uuid::{exec_ts_from_uuid7, exec_uuid, exec_uuidblob, exec_uuidstr, UuidFunc};
1+
use crate::{function::ExternalFunc, Database};
2+
use limbo_extension::{ExtensionApi, ResultCode, ScalarFunction, RESULT_ERROR, RESULT_OK};
3+
pub use limbo_extension::{Value as ExtValue, ValueType as ExtValueType};
4+
use std::{
5+
ffi::{c_char, c_void, CStr},
6+
rc::Rc,
7+
};
58

6-
#[derive(Debug, Clone, PartialEq)]
7-
pub enum ExtFunc {
8-
#[cfg(feature = "uuid")]
9-
Uuid(UuidFunc),
9+
extern "C" fn register_scalar_function(
10+
ctx: *mut c_void,
11+
name: *const c_char,
12+
func: ScalarFunction,
13+
) -> ResultCode {
14+
let c_str = unsafe { CStr::from_ptr(name) };
15+
let name_str = match c_str.to_str() {
16+
Ok(s) => s.to_string(),
17+
Err(_) => return RESULT_ERROR,
18+
};
19+
if ctx.is_null() {
20+
return RESULT_ERROR;
21+
}
22+
let db = unsafe { &*(ctx as *const Database) };
23+
db.register_scalar_function_impl(name_str, func)
1024
}
1125

12-
#[allow(unreachable_patterns)] // TODO: remove when more extension funcs added
13-
impl std::fmt::Display for ExtFunc {
14-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15-
match self {
16-
#[cfg(feature = "uuid")]
17-
Self::Uuid(uuidfn) => write!(f, "{}", uuidfn),
18-
_ => write!(f, "unknown"),
19-
}
26+
impl Database {
27+
fn register_scalar_function_impl(&self, name: String, func: ScalarFunction) -> ResultCode {
28+
self.syms.borrow_mut().functions.insert(
29+
name.to_string(),
30+
Rc::new(ExternalFunc {
31+
name: name.to_string(),
32+
func,
33+
}),
34+
);
35+
RESULT_OK
2036
}
21-
}
2237

23-
#[allow(unreachable_patterns)]
24-
impl ExtFunc {
25-
pub fn resolve_function(name: &str, num_args: usize) -> Option<ExtFunc> {
26-
match name {
27-
#[cfg(feature = "uuid")]
28-
name => UuidFunc::resolve_function(name, num_args),
29-
_ => None,
38+
pub fn build_limbo_extension(&self) -> ExtensionApi {
39+
ExtensionApi {
40+
ctx: self as *const _ as *mut c_void,
41+
register_scalar_function,
3042
}
3143
}
3244
}
33-
34-
pub fn init(db: &mut crate::Database) {
35-
#[cfg(feature = "uuid")]
36-
uuid::init(db);
37-
}

0 commit comments

Comments
 (0)