From f5cc3a08f0b6a1553bd4ea11490c8c3fa5b30e98 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Sun, 3 Mar 2024 09:19:06 +0200 Subject: [PATCH] Initial pass on SQLite C ABI This adds initial SQLite C ABI compatibility to Limbo to make sure we drive the Rust API in the right way that allows us to implement SQLite semantics. --- Cargo.lock | 126 ++++++++++++++++++++++++++++++++--- Cargo.toml | 1 + sqlite3/Cargo.toml | 19 ++++++ sqlite3/build.rs | 8 +++ sqlite3/cbindgen.toml | 7 ++ sqlite3/examples/example.c | 30 +++++++++ sqlite3/include/sqlite3.h | 44 +++++++++++++ sqlite3/src/lib.rs | 130 +++++++++++++++++++++++++++++++++++++ sqlite3/tests/.gitignore | 3 + sqlite3/tests/Makefile | 38 +++++++++++ sqlite3/tests/check.h | 11 ++++ sqlite3/tests/main.c | 16 +++++ sqlite3/tests/test-close.c | 11 ++++ sqlite3/tests/test-open.c | 30 +++++++++ 14 files changed, 465 insertions(+), 9 deletions(-) create mode 100644 sqlite3/Cargo.toml create mode 100644 sqlite3/build.rs create mode 100644 sqlite3/cbindgen.toml create mode 100644 sqlite3/examples/example.c create mode 100644 sqlite3/include/sqlite3.h create mode 100644 sqlite3/src/lib.rs create mode 100644 sqlite3/tests/.gitignore create mode 100644 sqlite3/tests/Makefile create mode 100644 sqlite3/tests/check.h create mode 100644 sqlite3/tests/main.c create mode 100644 sqlite3/tests/test-close.c create mode 100644 sqlite3/tests/test-open.c diff --git a/Cargo.lock b/Cargo.lock index 4f3a49cae..f708102db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,6 +110,17 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -161,6 +172,25 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "cbindgen" +version = "0.24.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b922faaf31122819ec80c4047cc684c6979a087366c069611e33649bf98e18d" +dependencies = [ + "clap 3.2.25", + "heck", + "indexmap 1.9.3", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 1.0.109", + "tempfile", + "toml", +] + [[package]] name = "cc" version = "1.0.83" @@ -209,6 +239,21 @@ dependencies = [ "half", ] +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_lex 0.2.4", + "indexmap 1.9.3", + "strsim", + "termcolor", + "textwrap", +] + [[package]] name = "clap" version = "4.4.2" @@ -227,7 +272,7 @@ checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" dependencies = [ "anstream", "anstyle", - "clap_lex", + "clap_lex 0.5.1", "strsim", ] @@ -243,6 +288,15 @@ dependencies = [ "syn 2.0.29", ] +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "clap_lex" version = "0.5.1" @@ -307,7 +361,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap", + "clap 4.4.2", "criterion-plot", "futures", "is-terminal", @@ -662,6 +716,12 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.0" @@ -678,7 +738,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown", + "hashbrown 0.14.0", ] [[package]] @@ -687,6 +747,15 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.3.2" @@ -708,6 +777,16 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.0.0" @@ -715,7 +794,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.0", ] [[package]] @@ -725,7 +804,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73c0fefcb6d409a6587c07515951495d482006f89a21daa0f2f783aa4fd5e027" dependencies = [ "ahash", - "indexmap", + "indexmap 2.0.0", "is-terminal", "itoa", "log", @@ -752,7 +831,7 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.2", "rustix", "windows-sys", ] @@ -818,7 +897,7 @@ name = "limbo" version = "0.0.0" dependencies = [ "anyhow", - "clap", + "clap 4.4.2", "cli-table", "dirs", "env_logger", @@ -853,6 +932,14 @@ dependencies = [ "sqlite3-parser", ] +[[package]] +name = "limbo_sqlite3" +version = "0.0.0" +dependencies = [ + "cbindgen", + "limbo_core", +] + [[package]] name = "linux-raw-sys" version = "0.4.5" @@ -962,7 +1049,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.2", "libc", ] @@ -993,6 +1080,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + [[package]] name = "parking_lot" version = "0.12.1" @@ -1458,7 +1551,7 @@ dependencies = [ "bitflags 2.4.0", "cc", "fallible-iterator 0.3.0", - "indexmap", + "indexmap 2.0.0", "log", "memchr", "phf", @@ -1559,6 +1652,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" + [[package]] name = "thiserror" version = "1.0.47" @@ -1589,6 +1688,15 @@ dependencies = [ "serde_json", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "uncased" version = "0.9.9" diff --git a/Cargo.toml b/Cargo.toml index 8a5b27ece..0d09960dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ resolver = "2" members = [ "bindings/wasm", "cli", + "sqlite3", "core", ] diff --git a/sqlite3/Cargo.toml b/sqlite3/Cargo.toml new file mode 100644 index 000000000..1a4f0ee13 --- /dev/null +++ b/sqlite3/Cargo.toml @@ -0,0 +1,19 @@ +# Copyright 2024 the Limbo authors. All rights reserved. MIT license. + +[package] +name = "limbo_sqlite3" +version = "0.0.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[lib] +crate-type = ["cdylib", "staticlib"] +doc = false + +[build-dependencies] +cbindgen = "0.24.0" + +[dependencies] +limbo_core = { path = "../core" } diff --git a/sqlite3/build.rs b/sqlite3/build.rs new file mode 100644 index 000000000..bf432e910 --- /dev/null +++ b/sqlite3/build.rs @@ -0,0 +1,8 @@ +use std::path::Path; + +fn main() { + let header_file = Path::new("include").join("sqlite3.h"); + cbindgen::generate(".") + .expect("Failed to generate C bindings") + .write_to_file(header_file); +} diff --git a/sqlite3/cbindgen.toml b/sqlite3/cbindgen.toml new file mode 100644 index 000000000..aba1a88aa --- /dev/null +++ b/sqlite3/cbindgen.toml @@ -0,0 +1,7 @@ +language = "C" +cpp_compat = true +include_guard = "LIMBO_SQLITE3_H" +line_length = 120 +no_includes = true +style = "type" +sys_includes = ["stdint.h"] diff --git a/sqlite3/examples/example.c b/sqlite3/examples/example.c new file mode 100644 index 000000000..2db23a73d --- /dev/null +++ b/sqlite3/examples/example.c @@ -0,0 +1,30 @@ +#include "sqlite3.h" + +#include +#include +#include + +int main(int argc, char *argv[]) +{ + sqlite3 *db; + int rc; + + rc = sqlite3_open("local.db", &db); + assert(rc == SQLITE_OK); + + sqlite3_stmt *stmt; + + rc = sqlite3_prepare_v2(db, "SELECT 'hello, world' AS message", -1, &stmt, NULL); + assert(rc == SQLITE_OK); + + rc = sqlite3_step(stmt); + assert(rc == SQLITE_OK); + + const unsigned char *result = sqlite3_column_text(stmt, 0); + + printf("result = %s\n", result); + + sqlite3_close(db); + + return 0; +} diff --git a/sqlite3/include/sqlite3.h b/sqlite3/include/sqlite3.h new file mode 100644 index 000000000..543d61258 --- /dev/null +++ b/sqlite3/include/sqlite3.h @@ -0,0 +1,44 @@ +#ifndef LIMBO_SQLITE3_H +#define LIMBO_SQLITE3_H + +#include + +#define SQLITE_OK 0 + +#define SQLITE_ERROR 1 + +#define SQLITE_BUSY 5 + +#define SQLITE_NOTFOUND 14 + +#define SQLITE_MISUSE 21 + +#define SQLITE_ROW 100 + +#define SQLITE_DONE 101 + +typedef struct sqlite3 sqlite3; + +typedef struct sqlite3_stmt sqlite3_stmt; + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +int sqlite3_open(const char *filename, sqlite3 **db_out); + +int sqlite3_close(sqlite3 *db); + +int sqlite3_prepare_v2(sqlite3 *db, const char *sql, int len, sqlite3_stmt **out_stmt, const char **tail); + +int sqlite3_finalize(sqlite3_stmt *stmt); + +int sqlite3_step(sqlite3_stmt *db); + +const unsigned char *sqlite3_column_text(sqlite3_stmt *db, int idx); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus + +#endif /* LIMBO_SQLITE3_H */ diff --git a/sqlite3/src/lib.rs b/sqlite3/src/lib.rs new file mode 100644 index 000000000..573414bb1 --- /dev/null +++ b/sqlite3/src/lib.rs @@ -0,0 +1,130 @@ +#![allow(non_camel_case_types)] + +use std::rc::Rc; +use std::ffi; + +pub const SQLITE_OK: ffi::c_int = 0; +pub const SQLITE_ERROR: ffi::c_int = 1; +pub const SQLITE_BUSY: ffi::c_int = 5; +pub const SQLITE_NOTFOUND: ffi::c_int = 14; +pub const SQLITE_MISUSE: ffi::c_int = 21; +pub const SQLITE_ROW: ffi::c_int = 100; +pub const SQLITE_DONE: ffi::c_int = 101; + +pub struct sqlite3 { + pub(crate) db: limbo_core::Database, + pub(crate) conn: limbo_core::Connection, +} + +impl sqlite3 { + pub fn new(db: limbo_core::Database, conn: limbo_core::Connection) -> Self { + Self { db, conn } + } +} + +pub struct sqlite3_stmt { + pub(crate) stmt: limbo_core::Statement, +} + +impl sqlite3_stmt { + pub fn new(stmt: limbo_core::Statement) -> Self { + Self { stmt } + } +} + +#[no_mangle] +pub unsafe extern "C" fn sqlite3_open( + filename: *const ffi::c_char, + db_out: *mut *mut sqlite3, +) -> ffi::c_int { + if filename.is_null() { + return SQLITE_MISUSE; + } + if db_out.is_null() { + return SQLITE_MISUSE; + } + let filename = ffi::CStr::from_ptr(filename); + let filename = match filename.to_str() { + Ok(s) => s, + Err(_) => return SQLITE_MISUSE, + }; + let io = match limbo_core::PlatformIO::new() { + Ok(io) => Rc::new(io), + Err(_) => return SQLITE_MISUSE, + }; + match limbo_core::Database::open_file(io, filename) { + Ok(db) => { + let conn = db.connect(); + *db_out = Box::leak(Box::new(sqlite3::new(db, conn))); + SQLITE_OK + } + Err(e) => { + SQLITE_NOTFOUND + }, + } +} + +#[no_mangle] +pub unsafe extern "C" fn sqlite3_close(db: *mut sqlite3) -> ffi::c_int { + if db.is_null() { + return SQLITE_OK; + } + let _ = Box::from_raw(db); + SQLITE_OK +} + +#[no_mangle] +pub unsafe extern "C" fn sqlite3_prepare_v2( + db: *mut sqlite3, + sql: *const ffi::c_char, + len: ffi::c_int, + out_stmt: *mut *mut sqlite3_stmt, + tail: *mut *const ffi::c_char, +) -> ffi::c_int { + if db.is_null() || sql.is_null() || out_stmt.is_null() { + return SQLITE_MISUSE; + } + let db: &mut sqlite3 = &mut *db; + let sql = ffi::CStr::from_ptr(sql); + let sql = match sql.to_str() { + Ok(s) => s, + Err(_) => return SQLITE_MISUSE, + }; + let stmt = match db.conn.prepare(sql) { + Ok(stmt) => stmt, + Err(_) => return SQLITE_ERROR, + }; + *out_stmt = Box::leak(Box::new(sqlite3_stmt::new(stmt))); + SQLITE_OK +} + +#[no_mangle] +pub unsafe extern "C" fn sqlite3_finalize(stmt: *mut sqlite3_stmt) -> ffi::c_int { + if stmt.is_null() { + return SQLITE_MISUSE; + } + let _ = Box::from_raw(stmt); + SQLITE_OK +} + +#[no_mangle] +pub unsafe extern "C" fn sqlite3_step(db: *mut sqlite3_stmt) -> std::ffi::c_int { + let stmt = &mut *db; + if let Ok(result) = stmt.stmt.step() { + match result { + limbo_core::RowResult::IO => SQLITE_BUSY, + limbo_core::RowResult::Done => SQLITE_DONE, + limbo_core::RowResult::Row(_) => SQLITE_ROW, + } + } else { + SQLITE_ERROR + } +} + +#[no_mangle] +pub unsafe extern "C" fn sqlite3_column_text( + db: *mut sqlite3_stmt, + idx: std::ffi::c_int, +) -> *const std::ffi::c_uchar { + todo!(); +} diff --git a/sqlite3/tests/.gitignore b/sqlite3/tests/.gitignore new file mode 100644 index 000000000..6a3146e9e --- /dev/null +++ b/sqlite3/tests/.gitignore @@ -0,0 +1,3 @@ +sqlite3-tests +*.d +*.o diff --git a/sqlite3/tests/Makefile b/sqlite3/tests/Makefile new file mode 100644 index 000000000..ee2f2194c --- /dev/null +++ b/sqlite3/tests/Makefile @@ -0,0 +1,38 @@ +V = +ifeq ($(strip $(V)),) + E = @echo + Q = @ +else + E = @\# + Q = +endif +export E Q + +PROGRAM = sqlite3-tests + +CFLAGS = -g -Wall -std=c17 -MMD -MP + +LIBS ?= -lsqlite3 + +OBJS += main.o +OBJS += test-close.o +OBJS += test-open.o +OBJS += test-prepare.o + +all: $(PROGRAM) + +%.o: %.c + $(E) " CC " $@ + $(Q) $(CC) $(CFLAGS) -c $< -o $@ + +$(PROGRAM): $(OBJS) + $(E) " LINK " $@ + $(Q) $(CC) $(LIBS) -o $@ $^ + +clean: + $(E) " CLEAN" + $(Q) rm -f $(PROGRAM) + $(Q) rm -f $(OBJS) *.d +.PHONY: clean + +-include $(OBJS:.o=.d) diff --git a/sqlite3/tests/check.h b/sqlite3/tests/check.h new file mode 100644 index 000000000..dbf69e11c --- /dev/null +++ b/sqlite3/tests/check.h @@ -0,0 +1,11 @@ +#ifndef CHECK_H + +#define CHECK_EQUAL(expected, actual) \ + do { \ + if ((expected) != (actual)) { \ + fprintf(stderr, "%s:%d: Assertion failed: %d != %d\n", __FILE__, __LINE__, (expected), (actual)); \ + exit(1); \ + } \ + } while (0) + +#endif diff --git a/sqlite3/tests/main.c b/sqlite3/tests/main.c new file mode 100644 index 000000000..58590ea84 --- /dev/null +++ b/sqlite3/tests/main.c @@ -0,0 +1,16 @@ +extern void test_open_misuse(); +extern void test_open_not_found(); +extern void test_open_existing(); +extern void test_close(); +extern void test_prepare_misuse(); + +int main(int argc, char *argv[]) +{ + test_open_misuse(); + test_open_not_found(); + test_open_existing(); + test_close(); + test_prepare_misuse(); + + return 0; +} diff --git a/sqlite3/tests/test-close.c b/sqlite3/tests/test-close.c new file mode 100644 index 000000000..c444eea01 --- /dev/null +++ b/sqlite3/tests/test-close.c @@ -0,0 +1,11 @@ +#include +#include +#include +#include + +#include "check.h" + +void test_close(void) +{ + CHECK_EQUAL(SQLITE_OK, sqlite3_close(NULL)); +} diff --git a/sqlite3/tests/test-open.c b/sqlite3/tests/test-open.c new file mode 100644 index 000000000..dfc769a33 --- /dev/null +++ b/sqlite3/tests/test-open.c @@ -0,0 +1,30 @@ +#include "check.h" + +#include +#include +#include +#include + +void test_open_misuse(void) +{ + CHECK_EQUAL(SQLITE_MISUSE, sqlite3_open(NULL, NULL)); + + CHECK_EQUAL(SQLITE_MISUSE, sqlite3_open("local.db", NULL)); +} + +void test_open_not_found(void) +{ + sqlite3 *db; + + CHECK_EQUAL(SQLITE_CANTOPEN, sqlite3_open("not-found/local.db", &db)); +} + +// TODO: test_open_create + +void test_open_existing(void) +{ + sqlite3 *db; + + CHECK_EQUAL(SQLITE_OK, sqlite3_open("../../testing/hello.db", &db)); + CHECK_EQUAL(SQLITE_OK, sqlite3_close(db)); +}