diff --git a/core/mvcc/.gitignore b/core/mvcc/.gitignore index 2c96eb1b6..1e7caa9ea 100644 --- a/core/mvcc/.gitignore +++ b/core/mvcc/.gitignore @@ -1,2 +1,2 @@ -target/ Cargo.lock +target/ diff --git a/core/mvcc/Cargo.toml b/core/mvcc/Cargo.toml index ba0f2a813..c9e8d84ba 100644 --- a/core/mvcc/Cargo.toml +++ b/core/mvcc/Cargo.toml @@ -2,9 +2,10 @@ resolver = "2" members = [ "database", + "bindings/c", ] [profile.release] codegen-units = 1 panic = "abort" -strip = true +strip = true \ No newline at end of file diff --git a/core/mvcc/README.md b/core/mvcc/README.md index fa279fcba..e48c3ac45 100644 --- a/core/mvcc/README.md +++ b/core/mvcc/README.md @@ -7,6 +7,7 @@ The aim of the project is to provide a building block for implementing database * Main memory architecture, rows are accessed via an index * Optimistic multi-version concurrency control +* Rust and C APIs ## Experimental Evaluation diff --git a/core/mvcc/bindings/c/Cargo.toml b/core/mvcc/bindings/c/Cargo.toml new file mode 100644 index 000000000..a54329eca --- /dev/null +++ b/core/mvcc/bindings/c/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "mvcc-c" +version = "0.0.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] +doc = false + +[build-dependencies] +cbindgen = "0.24.0" + +[dependencies] +base64 = "0.21.0" +mvcc-rs = { path = "../../database", features = ["tokio"] } +tokio = { version = "1.27.0", features = ["full"] } +tracing = "0.1.37" +tracing-subscriber = { version = "0" } diff --git a/core/mvcc/bindings/c/build.rs b/core/mvcc/bindings/c/build.rs new file mode 100644 index 000000000..f418d0a9a --- /dev/null +++ b/core/mvcc/bindings/c/build.rs @@ -0,0 +1,8 @@ +use std::path::Path; + +fn main() { + let header_file = Path::new("include").join("mvcc.h"); + cbindgen::generate(".") + .expect("Failed to generate C bindings") + .write_to_file(header_file); +} diff --git a/core/mvcc/bindings/c/cbindgen.toml b/core/mvcc/bindings/c/cbindgen.toml new file mode 100644 index 000000000..b530dce1d --- /dev/null +++ b/core/mvcc/bindings/c/cbindgen.toml @@ -0,0 +1,6 @@ +language = "C" +cpp_compat = true +include_guard = "MVCC_H" +line_length = 120 +no_includes = true +style = "type" diff --git a/core/mvcc/bindings/c/include/mvcc.h b/core/mvcc/bindings/c/include/mvcc.h new file mode 100644 index 000000000..808c4047e --- /dev/null +++ b/core/mvcc/bindings/c/include/mvcc.h @@ -0,0 +1,27 @@ +#ifndef MVCC_H +#define MVCC_H + +typedef enum { + MVCC_OK = 0, + MVCC_IO_ERROR_WRITE = 778, +} MVCCError; + +typedef struct DbContext DbContext; + +typedef const DbContext *MVCCDatabaseRef; + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +MVCCDatabaseRef MVCCDatabaseOpen(const char *path); + +void MVCCDatabaseClose(MVCCDatabaseRef db); + +MVCCError MVCCDatabaseInsert(MVCCDatabaseRef db, uint64_t id, const uint8_t *value_ptr, uintptr_t value_len); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus + +#endif /* MVCC_H */ diff --git a/core/mvcc/bindings/c/src/errors.rs b/core/mvcc/bindings/c/src/errors.rs new file mode 100644 index 000000000..8b40ad2ba --- /dev/null +++ b/core/mvcc/bindings/c/src/errors.rs @@ -0,0 +1,5 @@ +#[repr(C)] +pub enum MVCCError { + MVCC_OK = 0, + MVCC_IO_ERROR_WRITE = 778, +} diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs new file mode 100644 index 000000000..6eeffdeca --- /dev/null +++ b/core/mvcc/bindings/c/src/lib.rs @@ -0,0 +1,95 @@ +#![allow(non_camel_case_types)] +#![allow(clippy::missing_safety_doc)] + +mod errors; +mod types; + +use errors::MVCCError; +use mvcc_rs::*; +use types::{DbContext, MVCCDatabaseRef}; + +/// cbindgen:ignore +type Clock = clock::LocalClock; + +/// cbindgen:ignore +type Storage = persistent_storage::JsonOnDisk; + +/// cbindgen:ignore +type Inner = database::DatabaseInner; + +/// cbindgen:ignore +type Db = database::Database>; + +static INIT_RUST_LOG: std::sync::Once = std::sync::Once::new(); + +#[no_mangle] +pub unsafe extern "C" fn MVCCDatabaseOpen(path: *const std::ffi::c_char) -> MVCCDatabaseRef { + INIT_RUST_LOG.call_once(|| { + tracing_subscriber::fmt::init(); + }); + + tracing::debug!("MVCCDatabaseOpen"); + + let clock = clock::LocalClock::new(); + let path = unsafe { std::ffi::CStr::from_ptr(path) }; + let path = match path.to_str() { + Ok(path) => path, + Err(_) => { + tracing::error!("Invalid UTF-8 path"); + return MVCCDatabaseRef::null(); + } + }; + tracing::debug!("mvccrs: opening persistent storage at {path}"); + let storage = crate::persistent_storage::JsonOnDisk::new(path); + let db = Db::new(clock, storage); + let runtime = tokio::runtime::Runtime::new().unwrap(); + let ctx = DbContext { db, runtime }; + let ctx = Box::leak(Box::new(ctx)); + MVCCDatabaseRef::from(ctx) +} + +#[no_mangle] +pub unsafe extern "C" fn MVCCDatabaseClose(db: MVCCDatabaseRef) { + tracing::debug!("MVCCDatabaseClose"); + if db.is_null() { + tracing::debug!("warning: `db` is null in MVCCDatabaseClose()"); + return; + } + let _ = unsafe { Box::from_raw(db.get_ref_mut()) }; +} + +#[no_mangle] +pub unsafe extern "C" fn MVCCDatabaseInsert( + db: MVCCDatabaseRef, + id: u64, + value_ptr: *const u8, + value_len: usize, +) -> MVCCError { + let db = db.get_ref(); + let value = std::slice::from_raw_parts(value_ptr, value_len); + let data = match std::str::from_utf8(value) { + Ok(value) => value.to_string(), + Err(_) => { + tracing::info!("Invalid UTF-8, let's base64 this fellow"); + use base64::{engine::general_purpose, Engine as _}; + general_purpose::STANDARD.encode(value) + } + }; + let (db, runtime) = (&db.db, &db.runtime); + let row = database::Row { id, data }; + tracing::debug!("MVCCDatabaseInsert: {row:?}"); + match runtime.block_on(async move { + let tx = db.begin_tx().await; + db.insert(tx, row).await?; + db.commit_tx(tx).await + }) { + Ok(_) => { + tracing::debug!("MVCCDatabaseInsert: success"); + MVCCError::MVCC_OK + } + Err(e) => { + tracing::error!("MVCCDatabaseInsert: {e}"); + MVCCError::MVCC_IO_ERROR_WRITE + } + } +} diff --git a/core/mvcc/bindings/c/src/types.rs b/core/mvcc/bindings/c/src/types.rs new file mode 100644 index 000000000..acb929dcd --- /dev/null +++ b/core/mvcc/bindings/c/src/types.rs @@ -0,0 +1,47 @@ +use crate::Db; + +#[repr(transparent)] +pub struct MVCCDatabaseRef { + ptr: *const DbContext, +} + +impl MVCCDatabaseRef { + pub fn null() -> MVCCDatabaseRef { + MVCCDatabaseRef { + ptr: std::ptr::null(), + } + } + + pub fn is_null(&self) -> bool { + self.ptr.is_null() + } + + pub fn get_ref(&self) -> &DbContext { + unsafe { &*(self.ptr) } + } + + #[allow(clippy::mut_from_ref)] + pub fn get_ref_mut(&self) -> &mut DbContext { + let ptr_mut = self.ptr as *mut DbContext; + unsafe { &mut (*ptr_mut) } + } +} + +#[allow(clippy::from_over_into)] +impl From<&DbContext> for MVCCDatabaseRef { + fn from(value: &DbContext) -> Self { + Self { ptr: value } + } +} + +#[allow(clippy::from_over_into)] +impl From<&mut DbContext> for MVCCDatabaseRef { + fn from(value: &mut DbContext) -> Self { + Self { ptr: value } + } +} + +pub struct DbContext { + pub(crate) db: Db, + pub(crate) runtime: tokio::runtime::Runtime, +} diff --git a/core/mvcc/database/src/lib.rs b/core/mvcc/database/src/lib.rs index b671c908f..d88011290 100644 --- a/core/mvcc/database/src/lib.rs +++ b/core/mvcc/database/src/lib.rs @@ -36,85 +36,3 @@ pub mod database; pub mod errors; pub mod persistent_storage; pub mod sync; - -#[cfg(feature = "c_bindings")] -mod c_bindings { - use super::*; - type Clock = clock::LocalClock; - type Storage = persistent_storage::JsonOnDisk; - type Inner = database::DatabaseInner; - type Db = database::Database>; - - static INIT_RUST_LOG: std::sync::Once = std::sync::Once::new(); - - #[repr(C)] - pub struct DbContext { - db: Db, - runtime: tokio::runtime::Runtime, - } - - #[no_mangle] - pub extern "C" fn mvccrs_new_database(path: *const std::ffi::c_char) -> *mut DbContext { - INIT_RUST_LOG.call_once(|| { - tracing_subscriber::fmt::init(); - }); - - tracing::debug!("mvccrs_new_database"); - - let clock = clock::LocalClock::new(); - let path = unsafe { std::ffi::CStr::from_ptr(path) }; - let path = match path.to_str() { - Ok(path) => path, - Err(_) => { - tracing::error!("Invalid UTF-8 path"); - return std::ptr::null_mut(); - } - }; - tracing::debug!("mvccrs: opening persistent storage at {path}"); - let storage = crate::persistent_storage::JsonOnDisk::new(path); - let db = Db::new(clock, storage); - let runtime = tokio::runtime::Runtime::new().unwrap(); - Box::into_raw(Box::new(DbContext { db, runtime })) - } - - #[no_mangle] - pub unsafe extern "C" fn mvccrs_free_database(db: *mut Db) { - tracing::debug!("mvccrs_free_database"); - let _ = Box::from_raw(db); - } - - #[no_mangle] - pub unsafe extern "C" fn mvccrs_insert( - db: *mut DbContext, - id: u64, - value_ptr: *const u8, - value_len: usize, - ) -> i32 { - let value = std::slice::from_raw_parts(value_ptr, value_len); - let data = match std::str::from_utf8(value) { - Ok(value) => value.to_string(), - Err(_) => { - tracing::info!("Invalid UTF-8, let's base64 this fellow"); - use base64::{engine::general_purpose, Engine as _}; - general_purpose::STANDARD.encode(value) - } - }; - let DbContext { db, runtime } = unsafe { &mut *db }; - let row = database::Row { id, data }; - tracing::debug!("mvccrs_insert: {row:?}"); - match runtime.block_on(async move { - let tx = db.begin_tx().await; - db.insert(tx, row).await?; - db.commit_tx(tx).await - }) { - Ok(_) => { - tracing::debug!("mvccrs_insert: success"); - 0 // SQLITE_OK - } - Err(e) => { - tracing::error!("mvccrs_insert: {e}"); - 778 // SQLITE_IOERR_WRITE - } - } - } -}