diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2af2bc17..e718ad33 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -87,6 +87,36 @@ jobs: RUSTFLAGS: -Ctarget-feature=+crt-static -Cdebuginfo=1 run: cargo build --features bundled + test-wasm32-unknown-unknown: + name: Test ${{ matrix.os }} (wasm32-unknown-unknown) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v3 + # This has a matcher for test panics, so we use it even though elsewhere + # we use actions-rs/toolchain. + - uses: hecrj/setup-rust-action@v1 + with: + rust-version: stable${{ matrix.host }} + targets: ${{ matrix.target }} + + - name: Install Homebrew llvm on macOS + if: matrix.os == 'macos-latest' + run: brew install llvm + + - name: Add llvm path on Windows + if: matrix.os == 'windows-latest' + run: echo "C:\msys64\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - run: npm install -g wasm-pack + + - name: Test wasm-pack ${{ matrix.host }} + run: wasm-pack test --node --features bundled + test-sqlcipher-bundled: name: Test ${{ matrix.os }} (bundled SQLcipher + OpenSSL) runs-on: ${{ matrix.os }} diff --git a/Cargo.toml b/Cargo.toml index 13ea44f5..1ed430c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,22 +1,22 @@ [package] name = "rusqlite" # Note: Update version in README.md when you change this. -version = "0.32.1" authors = ["The rusqlite developers"] -edition = "2021" +categories = ["database"] description = "Ergonomic wrapper for SQLite" -repository = "https://github.com/rusqlite/rusqlite" documentation = "https://docs.rs/rusqlite/" -readme = "README.md" -keywords = ["sqlite", "database", "ffi"] +edition = "2021" +keywords = ["database", "ffi", "sqlite"] license = "MIT" -categories = ["database"] +readme = "README.md" +repository = "https://github.com/rusqlite/rusqlite" +version = "0.32.1" exclude = [ - "/.github/*", "/.gitattributes", - "/appveyor.yml", + "/.github/*", "/Changelog.md", + "/appveyor.yml", "/clippy.toml", "/codecov.yml", ] @@ -44,28 +44,28 @@ functions = [] # sqlite3_log: 3.6.23 (2010-03-09) trace = [] # sqlite3_db_release_memory: 3.7.10 (2012-01-16) -release_memory = [] +buildtime_bindgen = ["libsqlite3-sys/buildtime_bindgen"] bundled = ["libsqlite3-sys/bundled", "modern_sqlite"] -bundled-sqlcipher = ["libsqlite3-sys/bundled-sqlcipher", "bundled"] +bundled-sqlcipher = ["bundled", "libsqlite3-sys/bundled-sqlcipher"] bundled-sqlcipher-vendored-openssl = [ - "libsqlite3-sys/bundled-sqlcipher-vendored-openssl", "bundled-sqlcipher", + "libsqlite3-sys/bundled-sqlcipher-vendored-openssl", ] -buildtime_bindgen = ["libsqlite3-sys/buildtime_bindgen"] -limits = [] -loadable_extension = ["libsqlite3-sys/loadable_extension"] hooks = [] -preupdate_hook = ["libsqlite3-sys/preupdate_hook", "hooks"] i128_blob = [] +limits = [] +loadable_extension = ["libsqlite3-sys/loadable_extension"] +preupdate_hook = ["hooks", "libsqlite3-sys/preupdate_hook"] +release_memory = [] sqlcipher = ["libsqlite3-sys/sqlcipher"] unlock_notify = ["libsqlite3-sys/unlock_notify"] # xSavepoint, xRelease and xRollbackTo: 3.7.7 (2011-06-23) -vtab = [] csvtab = ["csv", "vtab"] +vtab = [] # pointer passing interfaces: 3.20.0 array = ["vtab"] # session extension: 3.13.0 -session = ["libsqlite3-sys/session", "hooks"] +session = ["hooks", "libsqlite3-sys/session"] # window functions: 3.25.0 window = ["functions"] # 3.9.0 @@ -73,13 +73,13 @@ series = ["vtab"] # check for invalid query. extra_check = [] # ]3.14.0, last] -modern_sqlite = ["libsqlite3-sys/bundled_bindings"] -in_gecko = ["modern_sqlite", "libsqlite3-sys/in_gecko"] bundled-windows = ["libsqlite3-sys/bundled-windows"] +in_gecko = ["libsqlite3-sys/in_gecko", "modern_sqlite"] +modern_sqlite = ["libsqlite3-sys/bundled_bindings"] # Build bundled sqlite with -fsanitize=address -with-asan = ["libsqlite3-sys/with-asan"] column_decltype = [] wasm32-wasi-vfs = ["libsqlite3-sys/wasm32-wasi-vfs"] +with-asan = ["libsqlite3-sys/with-asan"] # 3.23.0 serialize = ["modern_sqlite"] @@ -91,7 +91,6 @@ modern-full = [ "array", "backup", "blob", - "modern_sqlite", "chrono", "collation", "column_decltype", @@ -103,6 +102,7 @@ modern-full = [ "jiff", "limits", "load_extension", + "modern_sqlite", "serde_json", "serialize", "series", @@ -115,37 +115,38 @@ modern-full = [ "window", ] -bundled-full = ["modern-full", "bundled"] +bundled-full = ["bundled", "modern-full"] [dependencies] +bitflags = "2.6.0" +chrono = { version = "0.4.38", optional = true, default-features = false, features = [ + "clock", +] } +csv = { version = "1.1", optional = true } +fallible-iterator = "0.3" +fallible-streaming-iterator = "0.1" +hashlink = "0.9" jiff = { version = "0.1", optional = true, default-features = false, features = [ "std", ] } +rusqlite-macros = { path = "rusqlite-macros", version = "0.3.0", optional = true } +serde_json = { version = "1.0", optional = true } +smallvec = "1.6.1" time = { version = "0.3.36", features = [ "formatting", "macros", "parsing", ], optional = true } -bitflags = "2.6.0" -hashlink = "0.9" -chrono = { version = "0.4.38", optional = true, default-features = false, features = [ - "clock", -] } -serde_json = { version = "1.0", optional = true } -csv = { version = "1.1", optional = true } url = { version = "2.1", optional = true } -fallible-iterator = "0.3" -fallible-streaming-iterator = "0.1" uuid = { version = "1.0", optional = true } -smallvec = "1.6.1" -rusqlite-macros = { path = "rusqlite-macros", version = "0.3.0", optional = true } [dev-dependencies] doc-comment = "0.3" -tempfile = "3.1.0" regex = "1.5.5" -uuid = { version = "1.0", features = ["v4"] } +tempfile = "3.1.0" unicase = "2.6.0" +uuid = { version = "1.0", features = ["v4"] } +wasm-bindgen-test = "0.3" # Use `bencher` over criterion because it builds much faster, # and we don't have many benchmarks bencher = "0.1" @@ -158,8 +159,8 @@ version = "0.30.1" name = "auto_ext" [[test]] -name = "config_log" harness = false +name = "config_log" [[test]] name = "deny_single_threaded_sqlite_config" @@ -168,29 +169,29 @@ name = "deny_single_threaded_sqlite_config" name = "vtab" [[bench]] -name = "cache" harness = false +name = "cache" [[bench]] -name = "exec" harness = false +name = "exec" [[example]] -name = "loadable_extension" crate-type = ["cdylib"] -required-features = ["loadable_extension", "functions", "trace"] +name = "loadable_extension" +required-features = ["functions", "loadable_extension", "trace"] [[example]] name = "load_extension" -required-features = ["load_extension", "bundled", "functions", "trace"] +required-features = ["bundled", "functions", "load_extension", "trace"] [package.metadata.docs.rs] -features = ["modern-full", "rusqlite-macros"] all-features = false -no-default-features = true default-target = "x86_64-unknown-linux-gnu" +features = ["modern-full", "rusqlite-macros"] +no-default-features = true rustdoc-args = ["--cfg", "docsrs"] [package.metadata.playground] -features = ["bundled-full"] all-features = false +features = ["bundled-full"] diff --git a/libsqlite3-sys/Cargo.toml b/libsqlite3-sys/Cargo.toml index f6a15070..6fc4e09a 100644 --- a/libsqlite3-sys/Cargo.toml +++ b/libsqlite3-sys/Cargo.toml @@ -1,25 +1,28 @@ [package] -name = "libsqlite3-sys" -version = "0.30.1" authors = ["The rusqlite developers"] -edition = "2021" -repository = "https://github.com/rusqlite/rusqlite" +build = "build.rs" +categories = ["external-ffi-bindings"] description = "Native bindings to the libsqlite3 library" +edition = "2021" +keywords = ["ffi", "sqlcipher", "sqlite"] license = "MIT" links = "sqlite3" -build = "build.rs" -keywords = ["sqlite", "sqlcipher", "ffi"] -categories = ["external-ffi-bindings"] +name = "libsqlite3-sys" +repository = "https://github.com/rusqlite/rusqlite" +version = "0.30.1" [features] -default = ["min_sqlite_version_3_14_0"] -bundled = ["cc", "bundled_bindings"] -bundled-windows = ["cc", "bundled_bindings"] -bundled-sqlcipher = ["bundled"] -bundled-sqlcipher-vendored-openssl = ["bundled-sqlcipher", "openssl-sys/vendored"] buildtime_bindgen = ["bindgen", "pkg-config", "vcpkg"] -sqlcipher = [] +bundled = ["bundled_bindings", "cc"] +bundled-sqlcipher = ["bundled"] +bundled-sqlcipher-vendored-openssl = [ + "bundled-sqlcipher", + "openssl-sys/vendored", +] +bundled-windows = ["bundled_bindings", "cc"] +default = ["min_sqlite_version_3_14_0"] min_sqlite_version_3_14_0 = ["pkg-config", "vcpkg"] +sqlcipher = [] # Bundle only the bindings file. Note that this does nothing if # `buildtime_bindgen` is enabled. bundled_bindings = [] @@ -29,22 +32,33 @@ unlock_notify = [] # 3.13.0 preupdate_hook = ["buildtime_bindgen"] # 3.13.0 -session = ["preupdate_hook", "buildtime_bindgen"] in_gecko = [] -with-asan = [] +session = ["buildtime_bindgen", "preupdate_hook"] wasm32-wasi-vfs = [] +with-asan = [] [dependencies] openssl-sys = { version = "0.9.103", optional = true } +[target.wasm32-unknown-unknown.dependencies] +getrandom = { version = "0.2", features = ["js"] } +js-sys = "0.3" +wasm32-unknown-unknown-openbsd-libc = "0.2" + [build-dependencies] -bindgen = { version = "0.70", optional = true, default-features = false, features = ["runtime"] } -pkg-config = { version = "0.3.19", optional = true } +bindgen = { version = "0.70", optional = true, default-features = false, features = [ + "runtime", +] } cc = { version = "1.1.6", optional = true } +pkg-config = { version = "0.3.19", optional = true } vcpkg = { version = "0.2.15", optional = true } # for loadable_extension: -prettyplease = {version = "0.2.20", optional = true } +prettyplease = { version = "0.2.20", optional = true } # like bindgen quote = { version = "1.0.36", optional = true, default-features = false } # like bindgen -syn = { version = "2.0.72", optional = true, features = ["full", "extra-traits", "visit-mut"] } +syn = { version = "2.0.72", optional = true, features = [ + "extra-traits", + "full", + "visit-mut", +] } diff --git a/libsqlite3-sys/build.rs b/libsqlite3-sys/build.rs index 7a3d17bd..b4cab115 100644 --- a/libsqlite3-sys/build.rs +++ b/libsqlite3-sys/build.rs @@ -136,7 +136,6 @@ mod build_bundled { .flag("-DSQLITE_THREADSAFE=1") .flag("-DSQLITE_USE_URI") .flag("-DHAVE_USLEEP=1") - .flag("-DHAVE_ISNAN") .flag("-D_POSIX_THREAD_SAFE_FUNCTIONS") // cross compile with MinGW .warnings(false); @@ -236,6 +235,10 @@ mod build_bundled { cfg.static_crt(true); } + if env::var("TARGET") != Ok("wasm32-unknown-unknown".to_string()) { + cfg.flag("-DHAVE_ISNAN"); + } + if !win_target() { cfg.flag("-DHAVE_LOCALTIME_R"); } @@ -253,6 +256,56 @@ mod build_bundled { cfg.file("sqlite3/wasm32-wasi-vfs.c"); } } + if env::var("TARGET") == Ok("wasm32-unknown-unknown".to_string()) { + // Apple clang doesn't support wasm32, so use Homebrew clang by default. + if env::var("HOST") == Ok("x86_64-apple-darwin".to_string()) { + if env::var("CC").is_err() { + std::env::set_var("CC", "/usr/local/opt/llvm/bin/clang"); + } + if env::var("AR").is_err() { + std::env::set_var("AR", "/usr/local/opt/llvm/bin/llvm-ar"); + } + } else if env::var("HOST") == Ok("aarch64-apple-darwin".to_string()) { + if env::var("CC").is_err() { + std::env::set_var("CC", "/opt/homebrew/opt/llvm/bin/clang"); + } + if env::var("AR").is_err() { + std::env::set_var("AR", "/opt/homebrew/opt/llvm/bin/llvm-ar"); + } + } + + cfg.flag("-DSQLITE_OS_OTHER") + .flag("-DSQLITE_TEMP_STORE=3") + .flag("-DSQLITE_OMIT_LOCALTIME") + .flag("-Wno-incompatible-library-redeclaration"); + + let supported_features = [ + "atomics", + "bulk-memory", + "exception-handling", + "multivalue", + "mutable-globals", + "nontrapping-fptoint", + "reference-types", + "relaxed-simd", + "sign-ext", + "simd128", + ]; + for feature in env::var("CARGO_CFG_TARGET_FEATURE") + .unwrap_or_default() + .split(',') + { + if supported_features.contains(&feature) { + cfg.flag(format!("-m{feature}")); + } + } + + cfg.include( + std::env::var_os("DEP_WASM32_UNKNOWN_UNKNOWN_OPENBSD_LIBC_INCLUDE").unwrap(), + ); + + println!("cargo:rustc-link-lib=wasm32-unknown-unknown-openbsd-libc"); + } if cfg!(feature = "unlock_notify") { cfg.flag("-DSQLITE_ENABLE_UNLOCK_NOTIFY"); } diff --git a/libsqlite3-sys/src/lib.rs b/libsqlite3-sys/src/lib.rs index 7682776c..ecd37c33 100644 --- a/libsqlite3-sys/src/lib.rs +++ b/libsqlite3-sys/src/lib.rs @@ -1,5 +1,8 @@ #![expect(non_snake_case, non_camel_case_types)] +#[cfg(all(target_arch = "wasm32", target_os = "unknown"))] +mod wasm32_unknown_unknown; + // force linking to openssl #[cfg(feature = "bundled-sqlcipher-vendored-openssl")] extern crate openssl_sys; diff --git a/libsqlite3-sys/src/wasm32_unknown_unknown.rs b/libsqlite3-sys/src/wasm32_unknown_unknown.rs new file mode 100644 index 00000000..c3054069 --- /dev/null +++ b/libsqlite3-sys/src/wasm32_unknown_unknown.rs @@ -0,0 +1,175 @@ +#[cfg(not(feature = "bundled"))] +compile_error!("wasm32-unknown-unknown must be built with '--features bundled'"); + +use crate::{sqlite3_file, sqlite3_vfs, sqlite3_vfs_register, SQLITE_IOERR, SQLITE_OK}; +use std::os::raw::{c_char, c_int, c_void}; +use std::ptr::null_mut; + +#[no_mangle] +pub unsafe extern "C" fn sqlite3_os_init() -> c_int { + let vfs = sqlite3_vfs { + iVersion: 1, + szOsFile: 0, + mxPathname: 1024, + pNext: null_mut(), + zName: "libsqlite3-sys\0".as_ptr() as *const c_char, + pAppData: null_mut(), + xOpen: Some(wasm_vfs_open), + xDelete: Some(wasm_vfs_delete), + xAccess: Some(wasm_vfs_access), + xFullPathname: Some(wasm_vfs_fullpathname), + xDlOpen: Some(wasm_vfs_dlopen), + xDlError: Some(wasm_vfs_dlerror), + xDlSym: Some(wasm_vfs_dlsym), + xDlClose: Some(wasm_vfs_dlclose), + xRandomness: Some(wasm_vfs_randomness), + xSleep: Some(wasm_vfs_sleep), + xCurrentTime: Some(wasm_vfs_currenttime), + xGetLastError: None, + xCurrentTimeInt64: None, + xSetSystemCall: None, + xGetSystemCall: None, + xNextSystemCall: None, + }; + + sqlite3_vfs_register(Box::leak(Box::new(vfs)), 1) +} + +#[cfg(all(target_arch = "wasm32", target_os = "unknown"))] +const ALIGN: usize = 8; + +#[no_mangle] +pub unsafe extern "C" fn malloc(size: usize) -> *mut u8 { + let layout = match std::alloc::Layout::from_size_align(size + ALIGN, ALIGN) { + Ok(layout) => layout, + Err(_) => return null_mut(), + }; + + let ptr = std::alloc::alloc(layout); + if ptr.is_null() { + return null_mut(); + } + + *(ptr as *mut usize) = size; + ptr.offset(ALIGN as isize) +} + +#[no_mangle] +pub unsafe extern "C" fn free(ptr: *mut u8) { + let ptr = ptr.offset(-(ALIGN as isize)); + let size = *(ptr as *mut usize); + let layout = std::alloc::Layout::from_size_align_unchecked(size + ALIGN, ALIGN); + + std::alloc::dealloc(ptr, layout); +} + +#[no_mangle] +pub unsafe extern "C" fn realloc(ptr: *mut u8, new_size: usize) -> *mut u8 { + let ptr = ptr.offset(-(ALIGN as isize)); + let size = *(ptr as *mut usize); + let layout = std::alloc::Layout::from_size_align_unchecked(size + ALIGN, ALIGN); + + let ptr = std::alloc::realloc(ptr, layout, new_size + ALIGN); + if ptr.is_null() { + return null_mut(); + } + + *(ptr as *mut usize) = new_size; + ptr.offset(ALIGN as isize) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_vfs_open( + _arg1: *mut sqlite3_vfs, + _zName: *const c_char, + _arg2: *mut sqlite3_file, + _flags: c_int, + _pOutFlags: *mut c_int, +) -> c_int { + SQLITE_IOERR +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_vfs_delete( + _arg1: *mut sqlite3_vfs, + _zName: *const c_char, + _syncDir: c_int, +) -> c_int { + SQLITE_IOERR +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_vfs_access( + _arg1: *mut sqlite3_vfs, + _zName: *const c_char, + _flags: c_int, + _pResOut: *mut c_int, +) -> c_int { + SQLITE_IOERR +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_vfs_fullpathname( + _arg1: *mut sqlite3_vfs, + _zName: *const c_char, + _nOut: c_int, + _zOut: *mut c_char, +) -> c_int { + SQLITE_IOERR +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_vfs_dlopen( + _arg1: *mut sqlite3_vfs, + _zFilename: *const c_char, +) -> *mut c_void { + null_mut() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_vfs_dlerror( + _arg1: *mut sqlite3_vfs, + _nByte: c_int, + _zErrMsg: *mut c_char, +) { + // no-op +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_vfs_dlsym( + _arg1: *mut sqlite3_vfs, + _arg2: *mut c_void, + _zSymbol: *const c_char, +) -> ::std::option::Option { + None +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_vfs_dlclose(_arg1: *mut sqlite3_vfs, _arg2: *mut c_void) { + // no-op +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_vfs_sleep(_arg1: *mut sqlite3_vfs, microseconds: c_int) -> c_int { + let target_date = js_sys::Date::now() + (microseconds as f64 / 1000.0); + while js_sys::Date::now() < target_date {} + SQLITE_OK +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_vfs_randomness( + _arg1: *mut sqlite3_vfs, + nByte: c_int, + zByte: *mut c_char, +) -> c_int { + let slice = std::slice::from_raw_parts_mut(zByte as *mut u8, nByte as usize); + getrandom::getrandom(slice).unwrap_or_else(|_| std::process::abort()); + SQLITE_OK +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_vfs_currenttime(_arg1: *mut sqlite3_vfs, pTime: *mut f64) -> c_int { + // https://en.wikipedia.org/wiki/Julian_day + *pTime = (js_sys::Date::now() / 86400000.0) + 2440587.5; + SQLITE_OK +} diff --git a/tests/wasm_pack.rs b/tests/wasm_pack.rs new file mode 100644 index 00000000..779f6c81 --- /dev/null +++ b/tests/wasm_pack.rs @@ -0,0 +1,58 @@ +// wasm-pack test --node --features bundled + +use rusqlite::{params, Connection}; + +#[derive(Debug)] +struct Person { + #[allow(dead_code)] + id: i32, + name: String, + data: Option>, +} + +#[wasm_bindgen_test::wasm_bindgen_test] +fn node_test() { + let conn = Connection::open_in_memory().unwrap(); + + conn.execute( + "CREATE TABLE person ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + data BLOB + )", + [], + ) + .unwrap(); + let me = Person { + id: 0, + name: "Steven".to_string(), + data: None, + }; + conn.execute( + "INSERT INTO person (name, data) VALUES (?1, ?2)", + params![me.name, me.data], + ) + .unwrap(); + + let person = conn + .query_row("SELECT id, name, data FROM person", [], |r| { + Ok(Person { + id: r.get(0).unwrap(), + name: r.get(1).unwrap(), + data: r.get(2).unwrap(), + }) + }) + .unwrap(); + + assert_eq!( + format!("{:?}", person), + r#"Person { id: 1, name: "Steven", data: None }"# + ); + + let _random: i64 = conn.query_row("SELECT random()", [], |r| r.get(0)).unwrap(); + + let current_year: i32 = conn + .query_row("SELECT cast(strftime('%Y') AS decimal)", [], |r| r.get(0)) + .unwrap(); + assert!((2022..2050).contains(¤t_year)); +}