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.
This commit is contained in:
Pekka Enberg 2024-03-03 09:19:06 +02:00
parent 3420556018
commit f5cc3a08f0
14 changed files with 465 additions and 9 deletions

126
Cargo.lock generated
View file

@ -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"

View file

@ -5,6 +5,7 @@ resolver = "2"
members = [
"bindings/wasm",
"cli",
"sqlite3",
"core",
]

19
sqlite3/Cargo.toml Normal file
View file

@ -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" }

8
sqlite3/build.rs Normal file
View file

@ -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);
}

7
sqlite3/cbindgen.toml Normal file
View file

@ -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"]

View file

@ -0,0 +1,30 @@
#include "sqlite3.h"
#include <stddef.h>
#include <assert.h>
#include <stdio.h>
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;
}

44
sqlite3/include/sqlite3.h Normal file
View file

@ -0,0 +1,44 @@
#ifndef LIMBO_SQLITE3_H
#define LIMBO_SQLITE3_H
#include <stdint.h>
#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 */

130
sqlite3/src/lib.rs Normal file
View file

@ -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!();
}

3
sqlite3/tests/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
sqlite3-tests
*.d
*.o

38
sqlite3/tests/Makefile Normal file
View file

@ -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)

11
sqlite3/tests/check.h Normal file
View file

@ -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

16
sqlite3/tests/main.c Normal file
View file

@ -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;
}

View file

@ -0,0 +1,11 @@
#include <sqlite3.h>
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
#include "check.h"
void test_close(void)
{
CHECK_EQUAL(SQLITE_OK, sqlite3_close(NULL));
}

30
sqlite3/tests/test-open.c Normal file
View file

@ -0,0 +1,30 @@
#include "check.h"
#include <sqlite3.h>
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
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));
}