diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b586808..759ce96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,8 @@ jobs: needs: - test - miri - - clippy + # temporary ignore bug in clippy + # - clippy steps: - run: exit 0 @@ -51,22 +52,22 @@ jobs: - name: Run tests run: cargo test ${{ matrix.features }} --release -- --nocapture - clippy: - name: clippy - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - submodules: true - - name: Install Rust clippy - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly - override: true - components: clippy - - uses: Swatinem/rust-cache@v1 - - name: "clippy --all" - run: cargo clippy --all --tests --all-features + # clippy: + # name: clippy + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v3 + # with: + # submodules: true + # - name: Install Rust clippy + # uses: actions-rs/toolchain@v1 + # with: + # toolchain: nightly + # override: true + # components: clippy + # - uses: Swatinem/rust-cache@v1 + # - name: "clippy --all" + # run: cargo clippy --all --tests --all-features miri: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d52dd4a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.cargo +/target diff --git a/Cargo.toml b/Cargo.toml index 8176e2d..5f81d2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,9 @@ [workspace] members = [ - "doublets-ffi", "doublets", + "doublets-ffi", + "doublets-ffi/examples/rust", # dev "dev-deps/mem-rs", diff --git a/README.md b/README.md index 6b1582e..346cf52 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ later A basic operations in doublets: ```rust -use doublets::{data, mem, unit, Doublets, Links}; +use doublets::{data, mem, unit, Doublets, DoubletsExt, Links}; fn main() -> Result<(), doublets::Error> { // use file as memory for doublets @@ -19,7 +19,7 @@ fn main() -> Result<(), doublets::Error> { let mut store = unit::Store::::new(mem)?; // create 1: 1 1 - it's point: link where source and target it self - let mut point = store.create_link(1, 1)?; + let point = store.create_link(1, 1)?; // `any` constant denotes any link let any = store.constants().any; diff --git a/cbindgen-cxx.toml b/cbindgen-cxx.toml new file mode 100644 index 0000000..4422c13 --- /dev/null +++ b/cbindgen-cxx.toml @@ -0,0 +1,112 @@ +language = "C++" + +############## Options for Wrapping the Contents of the Header ################# + +# header = "/* Text to put at the beginning of the generated file. Probably a license. */" +# trailer = "/* Text to put at the end of the generated file */" +# include_guard = "my_bindings_h" +pragma_once = true +# autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */" +include_version = false +# namespace = "my_namespace" +namespaces = [] +using_namespaces = [] +sys_includes = [] +includes = [] +no_includes = false +after_includes = """ + +template +using Box = T*; + +template +using ManuallyDrop = T; + +""" + +braces = "SameLine" +line_length = 100 +tab_width = 2 +documentation = true +documentation_style = "auto" +documentation_length = "full" +line_endings = "LF" # also "CR", "CRLF", "Native" + +style = "both" +sort_by = "Name" # default for `fn.sort_by` and `const.sort_by` +usize_is_size_t = true + +[defines] +unstable_backtrace = "UNSTABLE_BACKTRACE" +# "target_os = freebsd" = "DEFINE_FREEBSD" +# "feature = serde" = "DEFINE_SERDE" + +[export] +include = [] +exclude = [ + "Box", "ManuallyDrop" +] +# prefix = "CAPI_" +item_types = [] +renaming_overrides_prefixing = false + +[export.rename] + +[export.body] + +[export.mangle] + +[fn] +rename_args = "None" +# must_use = "MUST_USE_FUNC" +# no_return = "NO_RETURN" +# prefix = "START_FUNC" +# postfix = "END_FUNC" +args = "auto" +sort_by = "Name" + +[struct] +rename_fields = "None" +# must_use = "MUST_USE_STRUCT" +derive_constructor = false +derive_eq = false +derive_neq = false +derive_lt = false +derive_lte = false +derive_gt = false +derive_gte = false + +[enum] +rename_variants = "None" +# must_use = "MUST_USE_ENUM" +add_sentinel = false +prefix_with_name = false +derive_helper_methods = false +derive_const_casts = false +derive_mut_casts = false +# cast_assert_name = "ASSERT" +derive_tagged_enum_destructor = false +derive_tagged_enum_copy_constructor = false +enum_class = true +private_default_tagged_enum_constructor = false + +[const] +allow_static_const = true +allow_constexpr = true +sort_by = "Name" + +[macro_expansion] +# bitflags = false + +[parse] +parse_deps = true +include = ["doublets", "platform-data"] +exclude = [] +clean = false +extra_bindings = [] + +[parse.expand] +crates = ["doublets-ffi"] +all_features = true +default_features = true +features = [] \ No newline at end of file diff --git a/dev-deps/data-rs b/dev-deps/data-rs index 95b7d4d..cd3cd4f 160000 --- a/dev-deps/data-rs +++ b/dev-deps/data-rs @@ -1 +1 @@ -Subproject commit 95b7d4deb04e1ba34d2fa3187dca80cc7f62c78e +Subproject commit cd3cd4f1ece69b8342b099e39a7d3e8a014dc5c7 diff --git a/doublets-ffi/Cargo.toml b/doublets-ffi/Cargo.toml index f1672de..86f9bd7 100644 --- a/doublets-ffi/Cargo.toml +++ b/doublets-ffi/Cargo.toml @@ -4,17 +4,18 @@ version = "0.1.0" edition = "2021" [lib] -crate-type = ["cdylib", "staticlib"] +crate-type = ["staticlib", "lib"] [dependencies] -log = "0.4.14" -libc = "0.2.100" -tracing-subscriber = "0.3.3" -tracing-log = "0.1.2" -tracing = "0.1.29" doublets = { path = "../doublets" } ffi-attributes = { path = "ffi-attributes" } -env-decorators = { path = "env-decorators" } + +tap = { version = "1.0.1" } +log-panics = { version = "2.1.0" } + +crossbeam-channel = { version = "0.5.6" } +tracing = { version = "0.1.36" } +tracing-subscriber = { version = "0.3.15", features = ["env-filter", "json"] } [package.log] features = ["release_max_level_error"] diff --git a/doublets-ffi/env-decorators/Cargo.toml b/doublets-ffi/env-decorators/Cargo.toml deleted file mode 100644 index 7475483..0000000 --- a/doublets-ffi/env-decorators/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "env-decorators" -version = "0.1.0" -edition = "2021" - -[lib] -proc-macro = true - -[dependencies] -syn = "1.0.86" -proc-macro2 = "1.0.36" -quote = "1.0.15" \ No newline at end of file diff --git a/doublets-ffi/env-decorators/src/lib.rs b/doublets-ffi/env-decorators/src/lib.rs deleted file mode 100644 index b9bfca9..0000000 --- a/doublets-ffi/env-decorators/src/lib.rs +++ /dev/null @@ -1,78 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::{ - parse::{Parse, ParseStream}, - Expr, LitStr, Token, Type, -}; - -struct EnvInput { - env: String, - expr: Expr, -} - -impl Parse for EnvInput { - fn parse(input: ParseStream) -> syn::Result { - let env = input.parse::()?.value(); - input.parse::()?; - let expr = input.parse::()?; - Ok(EnvInput { env, expr }) - } -} - -#[proc_macro] -pub fn env_value(item: TokenStream) -> TokenStream { - let EnvInput { env, expr } = syn::parse_macro_input!(item as EnvInput); - let mut expr = quote! { #expr }; - std::env::var(env) - .unwrap_or_default() - .split_whitespace() - .rev() - .for_each(|name| { - let ty_name: proc_macro2::TokenStream = name.parse().unwrap(); - expr = quote! { - #ty_name :: new(#expr) - } - }); - TokenStream::from(expr) -} - -struct EnvType { - env: String, - pat: String, - default: Type, -} - -impl Parse for EnvType { - fn parse(input: ParseStream) -> syn::Result { - let env = input.parse::()?.value(); - input.parse::()?; - let pat = input.parse::()?.value(); - input.parse::()?; - let ty = input.parse::()?; - Ok(EnvType { - env, - pat, - default: ty, - }) - } -} - -#[proc_macro] -pub fn env_type(item: TokenStream) -> TokenStream { - let EnvType { env, pat, default } = syn::parse_macro_input!(item as EnvType); - let mut ty = quote! { #default }; - std::env::var(env) - .unwrap_or_default() - .split_whitespace() - .rev() - .for_each(|name| { - let new_pat = pat.replace('*', &ty.to_string()); - println!("{}", new_pat); - let pat_ty: proc_macro2::TokenStream = new_pat.parse().unwrap(); - let ty_name: proc_macro2::TokenStream = name.parse().unwrap(); - ty = quote! { - #ty_name #pat_ty - } - }); - TokenStream::from(ty) -} diff --git a/doublets-ffi/examples/rust/Cargo.toml b/doublets-ffi/examples/rust/Cargo.toml new file mode 100644 index 0000000..9df334d --- /dev/null +++ b/doublets-ffi/examples/rust/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "doublets-ffi-examples" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +doublets-ffi = { path = "../../../doublets-ffi" } +doublets = { path = "../../../doublets" } +tracing = { version = "0.1.36" } +# dev +termcolor = { version = "1.1.3" } diff --git a/doublets-ffi/examples/rust/examples/all-logs.rs b/doublets-ffi/examples/rust/examples/all-logs.rs new file mode 100644 index 0000000..93efdc0 --- /dev/null +++ b/doublets-ffi/examples/rust/examples/all-logs.rs @@ -0,0 +1,45 @@ +#![feature(const_option_ext)] + +use doublets_ffi::{ + export::{doublets_create_log_handle, doublets_free_log_handle}, + logging::{Format, Level}, + FFIContext, +}; +use std::{ + ffi::{c_char, CStr}, + ptr, +}; + +unsafe extern "C" fn callback(_: FFIContext, ptr: *const c_char) { + let cstr = CStr::from_ptr(ptr); + print!("{}", cstr.to_str().unwrap()); +} + +const FORMAT: &str = option_env!("RUST_EXAMPLES_FORMAT").unwrap_or("virgin"); + +fn main() { + let ctx = ptr::null_mut(); + let level = Level::Trace; + let use_ansi = true; + + let format = match &FORMAT.to_ascii_lowercase()[..] { + "virgin" => Format::Virgin, + "pretty" => Format::Pretty, + "json" => Format::Json, + _ => { + panic!("allow only: `virgin`, `pretty`, `json`") + } + }; + + unsafe { + let handle = doublets_create_log_handle(ctx, callback, level, format, use_ansi); + + tracing::error!("SOMETHING IS SERIOUSLY WRONG!!!"); + tracing::warn!("important informational messages; might indicate an error"); + tracing::info!("general informational messages relevant to users"); + tracing::debug!("diagnostics used for internal debugging of a library or application"); + tracing::trace!("very verbose diagnostic events"); + + doublets_free_log_handle(handle); + } +} diff --git a/doublets-ffi/examples/rust/examples/doublets-context.rs b/doublets-ffi/examples/rust/examples/doublets-context.rs new file mode 100644 index 0000000..44995aa --- /dev/null +++ b/doublets-ffi/examples/rust/examples/doublets-context.rs @@ -0,0 +1,58 @@ +use doublets::{ + data::{Flow, LinksConstants}, + Link, +}; +use doublets_ffi::{ + constants::Constants, + export::{doublets_create_log_handle, doublets_free_log_handle}, + logging::{Format, Level}, + store::{create, doublets_create_united_store_u64, StoreHandle}, + FFIContext, +}; +use std::{ + ffi::{c_char, CStr, CString}, + fs, + ptr::{null, null_mut}, +}; + +unsafe extern "C" fn callback(_: FFIContext, ptr: *const c_char) { + print!("{}", CStr::from_ptr(ptr).to_str().unwrap()); +} + +extern "C" fn create_cb(ctx: FFIContext, before: Link, after: Link) -> Flow +where + F: FnMut(Link, Link), +{ + let handler = unsafe { &mut *(ctx as *mut F) }; + (*handler)(before, after); + Flow::Continue +} + +unsafe fn magic_create(handle: &mut StoreHandle, mut handler: F) +where + F: FnMut(Link, Link), +{ + let ctx = &mut handler as *mut _; + let _ = create(handle, null(), 0, ctx as *mut _, create_cb::); +} + +fn main() { + unsafe { + let handle = + doublets_create_log_handle(null_mut(), callback, Level::Trace, Format::Virgin, true); + + let path = CString::new("doublets.links").unwrap(); + let mut store = doublets_create_united_store_u64( + path.as_ptr(), + Constants::from(LinksConstants::external()), + ) + .unwrap(); + + magic_create(&mut store, |before, after| { + print!("{before:?}\n{after:?}\n"); + }); + + doublets_free_log_handle(handle); + } + let _ = fs::remove_file("doublets.links"); +} diff --git a/doublets-ffi/examples/rust/examples/error-handling.rs b/doublets-ffi/examples/rust/examples/error-handling.rs new file mode 100644 index 0000000..dc1e392 --- /dev/null +++ b/doublets-ffi/examples/rust/examples/error-handling.rs @@ -0,0 +1,65 @@ +#![feature(cstr_from_bytes_until_nul)] + +use doublets::{ + data::{Flow, LinksConstants}, + Link, +}; +use doublets_ffi::{ + constants::Constants, + errors::{free_error, read_error}, + export::{doublets_create_log_handle, doublets_free_log_handle}, + logging::{Format, Level}, + store::{constants_from_store, create_unit_store, delete, free_store}, + utils::Fallible, + FFIContext, +}; +use std::{ + error::Error, + ffi::{c_char, CStr}, + fs, + ptr::null_mut, +}; + +unsafe extern "C" fn callback(_: FFIContext, ptr: *const c_char) { + print!("{}", CStr::from_ptr(ptr).to_str().unwrap()); +} + +extern "C" fn create_cb(_: FFIContext, _: Link, _: Link) -> Flow { + Flow::Continue +} + +fn main() -> Result<(), Box> { + unsafe { + let log_handle = + doublets_create_log_handle(null_mut(), callback, Level::Trace, Format::Virgin, true); + + let path = CStr::from_bytes_until_nul(b"doublets.links\0")?; + let mut handle = + create_unit_store::(path.as_ptr(), Constants::from(LinksConstants::external())) + .unwrap(); + + let any = constants_from_store::(&handle).any; + + let query = [1 /* not exists index */, any, any]; + let result = delete::(&mut handle, query.as_ptr(), 3, null_mut(), create_cb); + + if let Fallible::Err(error) = result { + let mut msg_buf = vec![0u8; 256]; + read_error::(msg_buf.as_mut_ptr().cast(), 256, &error); + + let str = CStr::from_bytes_until_nul(&msg_buf)?.to_str()?; + tracing::error!("{}", str); + + free_error::(error); + } else { + unreachable!() + } + + free_store::(handle); + + doublets_free_log_handle(log_handle); + } + let _ = fs::remove_file("doublets.links"); + + Ok(()) +} diff --git a/doublets-ffi/examples/rust/examples/log-context.rs b/doublets-ffi/examples/rust/examples/log-context.rs new file mode 100644 index 0000000..96a9983 --- /dev/null +++ b/doublets-ffi/examples/rust/examples/log-context.rs @@ -0,0 +1,62 @@ +#![feature(try_blocks)] + +use doublets_ffi::{ + export::{doublets_create_log_handle, doublets_free_log_handle}, + logging::{Format, Level}, + FFIContext, +}; +use std::{ + ffi::{c_char, CStr}, + io::{self, Write}, +}; +use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; + +unsafe extern "C" fn callback(ctx: FFIContext, ptr: *const c_char) { + let str = CStr::from_ptr(ptr).to_str().unwrap(); + let ctx = &mut *(ctx as *mut usize); + + let mut stdout = StandardStream::stdout(ColorChoice::Always); + + let _: io::Result<_> = try { + match *ctx % 5 { + 0..=1 => stdout.set_color( + ColorSpec::new() + .set_fg(Some(Color::Rgb(0, 0, 255))) + .set_bg(Some(Color::Rgb(255, 165, 0))), + )?, + 2 => stdout.set_color( + ColorSpec::new() + .set_fg(Some(Color::Rgb(255, 165, 0))) + .set_bg(Some(Color::Rgb(0, 0, 255))), + )?, + 3..=5 => stdout.set_color( + ColorSpec::new() + .set_fg(Some(Color::Rgb(0, 0, 255))) + .set_bg(Some(Color::Rgb(255, 165, 0))), + )?, + _ => unreachable!(), + } + + write!(&mut stdout, "{str}")?; + + stdout.reset()?; + }; + + *ctx += 1; +} + +fn main() { + let ctx = &mut 0usize as *mut usize; + unsafe { + let handle = + doublets_create_log_handle(ctx.cast(), callback, Level::Trace, Format::Virgin, false); + + tracing::error!("SOMETHING IS SERIOUSLY WRONG!!!"); + tracing::warn!("important informational messages; might indicate an error"); + tracing::info!("general informational messages relevant to users"); + tracing::debug!("diagnostics used for internal debugging of a library or application"); + tracing::trace!("very verbose diagnostic events"); + + doublets_free_log_handle(handle); + } +} diff --git a/doublets-ffi/ffi-attributes/Cargo.toml b/doublets-ffi/ffi-attributes/Cargo.toml index df5ede5..cf7ec7d 100644 --- a/doublets-ffi/ffi-attributes/Cargo.toml +++ b/doublets-ffi/ffi-attributes/Cargo.toml @@ -7,8 +7,6 @@ edition = "2021" proc-macro = true [dependencies] -syn = "1.0.80" -proc-macro2 = "1.0.30" -darling = "0.13.0" -quote = "1.0.10" -serde = { version = "1.0.130", features = ["derive"] } \ No newline at end of file +quote = { version = "1.0.21" } +proc-macro2 = { version = "1.0.43" } +syn = { version = "1.0.99", features = ["full", "extra-traits"] } \ No newline at end of file diff --git a/doublets-ffi/ffi-attributes/src/expand.rs b/doublets-ffi/ffi-attributes/src/expand.rs new file mode 100644 index 0000000..b6077d6 --- /dev/null +++ b/doublets-ffi/ffi-attributes/src/expand.rs @@ -0,0 +1,200 @@ +use crate::{prepare, AliasLine, SpecializeArgs}; +use proc_macro::Diagnostic; +use proc_macro2::{Ident, TokenStream}; +use quote::{quote, ToTokens}; + +use syn::{ + punctuated::Punctuated, Attribute, FnArg, GenericParam, ItemFn, PatType, ReturnType, Signature, + Token, Type, TypeParam, +}; + +fn filter_generics<'gen>( + generics: impl Iterator + 'gen, + param: &'gen Ident, +) -> impl Iterator + 'gen { + generics + .filter(move |&par| { + if let GenericParam::Type(TypeParam { ident, .. }) = par { + ident != param + } else { + true + } + }) + .cloned() +} + +fn prepare_fn_args<'args>( + fn_args: impl Iterator, + param: &Ident, + ident: &Type, +) { + for arg in fn_args { + match arg { + FnArg::Typed(PatType { box ty, .. }) => { + *ty = prepare::replace_ty_in_param(ty.clone(), param, ident); + } + FnArg::Receiver(_) => { + todo!() + } + } + } +} + +fn prepare_output_type(output: &mut ReturnType, param: &Ident, ident: &Type) { + if let ReturnType::Type(_, box ty) = output { + *ty = prepare::replace_ty_in_param(ty.clone(), param, ident); + } +} + +fn args_idents<'args>( + args: impl Iterator, +) -> impl Iterator { + args.map(|arg| match arg { + FnArg::Typed(PatType { box pat, .. }) => pat as &dyn ToTokens, + _ => { + todo!() + } + }) +} + +fn build_fn(input: ItemFn, real_fn: &Ident, param: &Type, attributes: &[Attribute]) -> TokenStream { + let ItemFn { + attrs, vis, sig, .. + } = input; + + let Signature { + output: return_type, + inputs: params, + unsafety, + asyncness, + constness, + abi, + ident, + generics: + syn::Generics { + params: gen_params, + where_clause, + .. + }, + .. + } = sig; + + let args = args_idents(params.iter()); + quote! { + // delegate attributes below the self one + #(#attrs)* + // delegate provided attributes + #(#attributes)* + #vis #constness #unsafety #asyncness #abi fn #ident<#gen_params>(#params) #return_type + #where_clause + { + #real_fn::<#param>(#(#args),*) + } + } +} + +fn gen_new_def(mut fn_list: TokenStream, input: ItemFn, args: SpecializeArgs) -> TokenStream { + let real_name = input.sig.ident.to_string(); + let name_pat = args + .name + .map(|name| name.to_token_stream().to_string()) + .unwrap_or_else(|| real_name + "_*"); + let name_pat = name_pat.trim_matches('"'); + + for AliasLine { ty, ident: lit, .. } in args.aliases { + let ItemFn { + attrs, + vis, + sig, + block, + } = input.clone(); + + let Signature { + output: mut return_type, + inputs: mut params, + generics: syn::Generics { + params: gen_params, .. + }, + .. + } = sig.clone(); + + let param = args.param.as_ref().unwrap(); + let generics: Punctuated<_, Token![,]> = + Punctuated::from_iter(filter_generics(gen_params.iter(), param)); + prepare_fn_args(params.iter_mut(), param, &ty); + prepare_output_type(&mut return_type, param, &ty); + + let new_ident: Ident = Ident::new( + &name_pat.replace('*', &lit.to_token_stream().to_string()), + lit.span(), + ); + + let real_fn = sig.ident.clone(); + let sig = Signature { + ident: new_ident, + output: return_type, + inputs: params, + generics: syn::Generics { + params: generics, + ..sig.generics + }, + ..sig + }; + let new_fn = build_fn( + ItemFn { + attrs, + vis, + sig, + block, + }, + &real_fn, + &ty, + &args.attributes, + ); + fn_list = quote! { + #fn_list + #new_fn + } + } + + fn_list +} + +pub(crate) fn gen_function(input: ItemFn, args: SpecializeArgs) -> TokenStream { + args.warnings().for_each(Diagnostic::emit); + + let ItemFn { + attrs, + vis, + sig, + block, + } = input.clone(); + + let Signature { + output: return_type, + inputs: params, + unsafety, + asyncness, + constness, + abi, + ident, + generics: + syn::Generics { + params: gen_params, + where_clause, + .. + }, + .. + } = sig; + + let this = quote! { + #(#attrs) * + #vis #constness #unsafety #asyncness #abi fn #ident<#gen_params>(#params) #return_type + #where_clause + { + #block + } + }; + + gen_new_def(this, input, args) +} diff --git a/doublets-ffi/ffi-attributes/src/lib.rs b/doublets-ffi/ffi-attributes/src/lib.rs index 82379cd..4547f3c 100644 --- a/doublets-ffi/ffi-attributes/src/lib.rs +++ b/doublets-ffi/ffi-attributes/src/lib.rs @@ -1,189 +1,192 @@ #![feature(box_syntax)] +#![feature(proc_macro_diagnostic)] +#![feature(box_patterns)] -use proc_macro::TokenStream; +mod expand; +mod prepare; -use darling::FromMeta; -use quote::{quote, ToTokens}; - -use syn::{parse::Parser, punctuated::Punctuated}; +use proc_macro::{Level, Span}; +use std::collections::HashMap; use syn::{ - parse_macro_input, AttributeArgs, FnArg, GenericArgument, GenericParam, Ident, ItemFn, - PathArguments, ReturnType, Type, + parse::{Parse, ParseStream}, + parse_macro_input, + punctuated::Punctuated, + spanned::Spanned, + token::Paren, + Attribute, Ident, ItemFn, LitStr, Token, Type, }; -fn csharp_convention(s: String) -> String { - match s.as_str() { - "i8" => "SByte", - "u8" => "Byte", - "i16" => "Int16", - "u16" => "UInt16", - "i32" => "Int32", - "u32" => "UInt32", - "i64" => "Int64", - "u64" => "UInt64", - s => { - panic!("{} is incompatible with doublets-ffi type", s) - } - } - .to_string() +mod kw { + syn::custom_keyword!(types); + syn::custom_keyword!(name); + syn::custom_keyword!(attributes); } -#[derive(FromMeta, PartialEq, Eq, Debug)] -#[allow(non_camel_case_types)] -enum Conventions { - csharp, +#[derive(Clone, Default, Debug)] +struct SpecializeArgs { + name: Option, + param: Option, + aliases: Punctuated, + attributes: Vec, + /// Errors describing any unrecognized parse inputs that we skipped. + parse_warnings: Vec, } -#[derive(FromMeta)] -struct MacroArgs { - convention: Conventions, - #[darling(multiple)] - types: Vec, - name: String, +impl SpecializeArgs { + pub(crate) fn warnings(&self) -> impl Iterator + '_ { + self.parse_warnings.iter().map(|err| { + let msg = format!("found unrecognized input, {}", err); + proc_macro::Diagnostic::spanned::, _>( + vec![err.span().unwrap()], + Level::Warning, + msg, + ) + }) + } } -fn ty_from_to(ty: Type, from: &str, to: &str) -> Type { - match ty { - Type::Array(arr) => ty_from_to(*arr.elem, from, to), - Type::Path(mut path) => { - path.path.segments.iter_mut().for_each(|seg| { - if seg.ident.to_string().as_str() == from { - seg.ident = Ident::from_string(to).unwrap(); +impl Parse for SpecializeArgs { + fn parse(input: ParseStream<'_>) -> syn::Result { + let mut args = Self::default(); + + while !input.is_empty() { + let lookahead = input.lookahead1(); + + if lookahead.peek(kw::name) { + if args.name.is_some() { + return Err(input.error("expected only a single `name` argument")); } - match seg.arguments { - PathArguments::AngleBracketed(ref mut angle) => { - for arg in angle.args.iter_mut() { - match arg { - GenericArgument::Type(gty) => { - *gty = ty_from_to(gty.clone(), from, to); - } - _ => { - panic!("not doublets-ffi compatible generic") - } - } - } - } - PathArguments::Parenthesized(_) => { - todo!() - } - _ => { /* ignore */ } + let name = input.parse::>()?.lit; + args.name = Some(name); + } else if lookahead.peek(kw::types) { + if !args.aliases.is_empty() { + return Err(input.error("expected only a single `types` argument")); } - }); - Type::Path(path) - } - Type::Ptr(mut ptr) => { - *ptr.elem = ty_from_to(*ptr.elem, from, to); - Type::Ptr(ptr) - } - Type::Reference(mut refer) => { - *refer.elem = ty_from_to(*refer.elem, from, to); - Type::Reference(refer) - } - _ => { - panic!("unexpected doublets-ffi type"); + let AliasArg { param, aliases, .. } = input.parse::()?; + args.param = Some(param); + args.aliases = aliases; + } else if lookahead.peek(kw::attributes) { + if !args.attributes.is_empty() { + return Err(input.error("expected only a single `attributes` argument")); + } + let _ = input.parse::()?; + let content; + let _ = syn::parenthesized!(content in input); + args.attributes = content.call(Attribute::parse_outer)?; + } else if lookahead.peek(Token![,]) { + let _ = input.parse::()?; + } else { + // Parse the unrecognized tokens stream, + // and ignore it away so we can keep parsing. + args.parse_warnings.push(lookahead.error()); + let _ = input.parse::(); + } } + + Ok(args) } } -#[proc_macro_attribute] -pub fn specialize_for(args: TokenStream, input: TokenStream) -> TokenStream { - let attr_args = parse_macro_input!(args as AttributeArgs); - let input = parse_macro_input!(input as ItemFn); - let input_clone: ItemFn = input.clone(); - let ident = input.sig.ident; - // TODO: use args - let args = match MacroArgs::from_list(&attr_args) { - Ok(v) => v, - Err(e) => { - return TokenStream::from(e.write_errors()); - } - }; - //println!("{:?}", args.types); - - let inputs = input.sig.inputs; - let generic_name = { - let mut generics_names: Vec<_> = input - .sig - .generics - .params - .iter() - .map(|param| match param { - GenericParam::Lifetime(_) => { - panic!("`lifetime` generic is not supported") - } - GenericParam::Const(_) => { - panic!("`const` generic is not supported") - } - GenericParam::Type(ty) => ty.ident.to_string(), - }) - .collect(); - assert_eq!(generics_names.len(), 1); - generics_names.remove(0) - }; - - let fn_pat = args.name; - let asterisk_count = fn_pat.chars().filter(|c| *c == '*').count(); - assert_eq!(asterisk_count, 1); - - let mut out = quote! { #input_clone }; - - for ty in args.types { - let ty_str = ty.as_str(); - let ty_tt: proc_macro2::TokenStream = ty.parse().unwrap(); - let fn_pat: proc_macro2::TokenStream = fn_pat - .replace( - '*', - match &args.convention { - Conventions::csharp => csharp_convention(ty.clone()), - _ => { - panic!("unknown convention") - } - } - .as_str(), - ) - .parse() - .unwrap(); +// custom_kw = "literal" +#[derive(Debug, Clone)] +#[allow(dead_code)] +struct StrArg { + kw: T, + // track issue: https://github.com/dtolnay/syn/issues/1209 + eq: syn::token::Eq, + lit: LitStr, +} - let mut inputs: Punctuated = inputs.clone(); +impl Parse for StrArg { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + kw: input.parse()?, + eq: input.parse()?, + lit: input.parse()?, + }) + } +} - let output_ty: proc_macro2::TokenStream = match &input.sig.output { - ReturnType::Default => "()".parse().unwrap(), - ReturnType::Type(_, ty) => { - ty_from_to((**ty).clone(), &generic_name, ty_str).to_token_stream() - } - }; +// MyType => mu_type_suffix +#[derive(Clone, Debug)] +#[allow(dead_code)] +struct AliasLine { + ty: Type, + to: Token![=>], + ident: Ident, +} - inputs.iter_mut().for_each(|arg| match arg { - FnArg::Receiver(_) => { - panic!("function with `self` is not supported") - } - FnArg::Typed(pat_type) => { - pat_type.ty = box ty_from_to(*(pat_type.ty).clone(), &generic_name, &ty); - } - }); - - let _generic_name: proc_macro2::TokenStream = generic_name.parse().unwrap(); - let input_args: Vec<_> = inputs - .iter() - .map(|arg| match arg { - FnArg::Receiver(_) => { - unreachable!() - } - FnArg::Typed(ty) => ty.pat.to_token_stream(), - }) - .collect(); - - out = quote! { - #out - #[no_mangle] - pub unsafe extern "C" fn #fn_pat(#inputs) -> #output_ty { - #ident::<#ty_tt>(#(#input_args),*) - } +impl Parse for AliasLine { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + ty: input.parse()?, + to: input.parse()?, + ident: input.parse()?, + }) + } +} + +// types::( +// u32 => integral, +// f32 => floating, +// (u32, Option) => magic, +// ) +#[allow(dead_code)] +struct AliasArg { + kw: kw::types, + colon: Token![::], + lt_token: Token![<], + param: Ident, + gt_toke: Token![>], + paren_token: Paren, + aliases: Punctuated, +} + +fn alias_validation(aliases: &Punctuated) -> Result<(), syn::Error> { + let mut map = HashMap::new(); + aliases.iter().try_for_each(|AliasLine { ty, ident, .. }| { + if let Some(twice) = map.insert(ty.clone(), ident.clone()) { + Err(syn::Error::new( + ty.span().join(ident.span()).unwrap(), + format!("tried to add alias to `{twice}` twice"), + )) + } else { + Ok(()) + } + }) +} + +impl Parse for AliasArg { + fn parse(input: ParseStream<'_>) -> syn::Result { + let content; + let new = Self { + kw: input.parse()?, + colon: input.parse()?, + lt_token: input.parse()?, + param: input.parse()?, + gt_toke: input.parse()?, + paren_token: syn::parenthesized!(content in input), + aliases: content.parse_terminated(AliasLine::parse)?, }; + + alias_validation(&new.aliases).map(|_| new) } +} - //println!("{}", out); +fn specialize_precise( + args: SpecializeArgs, + item: proc_macro::TokenStream, +) -> syn::Result { + let input = syn::parse::(item)?; + Ok(expand::gen_function(input, args).into()) +} - out.into() +#[proc_macro_attribute] +pub fn specialize_for( + args: proc_macro::TokenStream, + item: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + let args = parse_macro_input!(args as SpecializeArgs); + specialize_precise(args, item).unwrap_or_else(|_err| todo!()) } diff --git a/doublets-ffi/ffi-attributes/src/prepare.rs b/doublets-ffi/ffi-attributes/src/prepare.rs new file mode 100644 index 0000000..74b5e62 --- /dev/null +++ b/doublets-ffi/ffi-attributes/src/prepare.rs @@ -0,0 +1,67 @@ +use proc_macro2::Ident; +use syn::{ + GenericArgument, ParenthesizedGenericArguments, PathArguments, ReturnType, Type, TypePath, +}; + +pub(crate) fn prepare_path(mut path: TypePath, from: &Ident, to: &Type) -> Type { + if path.path.is_ident(from) { + return to.clone(); + } + path.path.segments.iter_mut().for_each(|seg| { + match &mut seg.arguments { + PathArguments::AngleBracketed(angle) => { + for arg in &mut angle.args { + match arg { + GenericArgument::Type(gty) => { + *gty = replace_ty_in_param(gty.clone(), from, to); + } + _ => { /* ignore */ } + } + } + } + PathArguments::Parenthesized(ParenthesizedGenericArguments { + inputs, output, .. + }) => { + for input in inputs { + *input = replace_ty_in_param(input.clone(), from, to); + } + if let ReturnType::Type(_, box ty) = output { + *ty = replace_ty_in_param(ty.clone(), from, to); + } + } + _ => { /* ignore */ } + } + }); + Type::Path(path) +} + +pub(crate) fn replace_ty_in_param(ty: Type, from: &Ident, to: &Type) -> Type { + match ty { + Type::Path(path) => prepare_path(path, from, to), + Type::Array(mut arr) => { + *arr.elem = replace_ty_in_param(*arr.elem, from, to); + Type::Array(arr) + } + Type::Ptr(mut ptr) => { + *ptr.elem = replace_ty_in_param(*ptr.elem, from, to); + Type::Ptr(ptr) + } + Type::Reference(mut refer) => { + *refer.elem = replace_ty_in_param(*refer.elem, from, to); + Type::Reference(refer) + } + Type::Slice(mut slice) => { + *slice.elem = replace_ty_in_param(*slice.elem, from, to); + Type::Slice(slice) + } + Type::Tuple(mut tuple) => { + for elem in &mut tuple.elems { + *elem = replace_ty_in_param(elem.clone(), from, to); + } + Type::Tuple(tuple) + } + _ => { + todo!() + } + } +} diff --git a/doublets-ffi/readme.md b/doublets-ffi/readme.md index 796bdfe..5ba422a 100644 --- a/doublets-ffi/readme.md +++ b/doublets-ffi/readme.md @@ -1,52 +1,70 @@ - - ## Build dynamic or static library ### Basic build library -Install [rustup](https://rustup.rs/) and setup Rust language tools. +Before you can start writing a binding over doublets library, you’ll need a version of Rust installed. +We recommend you use [rustup](https://rustup.rs/) to install or configure such latest version. + +Please note that some platforms support multiple variants of toolchains -on linux: -```shell -rustup toolchain install nightly -``` -on windows: ```shell -rustup toolchain install nightly-[gnu|msvc] +# windows +rustup toolchain install stable-[gnu|msvc] ``` Run cargo build in this folder: + ```shell -cargo +nightly build --release +# build with `dev` profile +cargo build +# build with `release` profile +cargo build --release ``` Great! Your libray is located in the `target/release` folder. ### Advanced build library + You can configure your build in the __`Cargo.toml`__ file: Try write the following code: + ```toml [profile.release] debug = true overflow-checks = true ``` -And rerun build +And rerun build What is it?\ `debug` - controls the amount of debug information included in the compiled binary.\ -`overflow-checks` - controls the behavior of runtime [integer overflow](https://doc.rust-lang.org/reference/expressions/operator-expr.html#overflow). +`overflow-checks` - controls the behavior of +runtime [integer overflow](https://doc.rust-lang.org/reference/expressions/operator-expr.html#overflow). + +Also, you can add it flags to `RUSTFLAGS` env: `RUSTFLAGS="-C debuginfo=2 -C overflow-checks=yes"` + +codegen flags:\ +[debuginfo](https://doc.rust-lang.org/rustc/codegen-options/index.html#debuginfo)\ +[overflow-checks](https://doc.rust-lang.org/rustc/codegen-options/index.html#overflow-checks) [More options](https://doc.rust-lang.org/cargo/reference/profiles.html) -Also you can configure log level.\ +Also you can configure compiler builtin log level.\ Try replace + ```toml [package.log] features = ["release_max_level_error"] ``` + To + ```toml [package.log] features = ["release_max_level_info"] -``` \ No newline at end of file +``` + +### Features + +You can build with `--cfg unstable_backtrace` in `RUSTFLAGS` +to enable `backtrace` feature and provide appropriate methods \ No newline at end of file diff --git a/doublets-ffi/src/constants.rs b/doublets-ffi/src/constants.rs new file mode 100644 index 0000000..bb485ed --- /dev/null +++ b/doublets-ffi/src/constants.rs @@ -0,0 +1,84 @@ +use crate::utils::Maybe; +use doublets::data::{LinkType, LinksConstants}; +use std::ops::RangeInclusive; + +/// FFI repr to [`Inclusive Range`] +/// +/// [`Inclusive Range`]: https://doc.rust-lang.org/std/ops/struct.RangeInclusive.html +#[derive(Eq, PartialEq)] +#[repr(C)] +pub struct Range { + start: T, + end: T, +} + +impl From> for Range { + fn from(range: RangeInclusive) -> Self { + Self { + start: *range.start(), + end: *range.end(), + } + } +} + +impl From> for RangeInclusive { + fn from(Range { start, end }: Range) -> Self { + RangeInclusive::new(start, end) + } +} + +#[repr(C)] +pub struct Constants { + pub index_part: T, + pub source_part: T, + pub target_part: T, + pub null: T, + pub r#continue: T, + pub r#break: T, + pub skip: T, + pub any: T, + pub itself: T, + pub error: T, + pub internal_range: Range, + pub external_range: Maybe>, +} + +impl From> for Constants { + fn from(c: LinksConstants) -> Self { + Self { + index_part: c.index_part, + source_part: c.source_part, + target_part: c.target_part, + null: c.null, + r#continue: c.r#continue, + r#break: c.r#break, + skip: c.skip, + any: c.any, + itself: c.itself, + error: c.error, + internal_range: Range::from(c.internal_range), + // external_range: c.external_range.map(|r| Range(*r.start(), *r.end())), + external_range: c.external_range.map(Range::from).into(), + } + } +} + +#[allow(clippy::from_over_into)] +impl Into> for Constants { + fn into(self) -> LinksConstants { + LinksConstants { + index_part: self.index_part, + source_part: self.source_part, + target_part: self.target_part, + r#break: self.r#break, + null: self.null, + r#continue: self.r#continue, + skip: self.skip, + any: self.any, + itself: self.itself, + error: self.error, + internal_range: RangeInclusive::from(self.internal_range), + external_range: Option::from(self.external_range).map(|range: Range<_>| range.into()), + } + } +} diff --git a/doublets-ffi/src/errors.rs b/doublets-ffi/src/errors.rs new file mode 100644 index 0000000..120f7cc --- /dev/null +++ b/doublets-ffi/src/errors.rs @@ -0,0 +1,127 @@ +use crate::c_char; +use doublets::{data::LinkType, Doublet, Link}; +#[cfg(unstable_backtrace)] +use std::backtrace::Backtrace; +use std::{ + cmp, error, + ffi::c_short, + fmt::{self, Debug, Display, Formatter}, + ptr, +}; + +type OpaqueError = Box; + +#[repr(C, usize)] +#[derive(Debug)] +pub enum DoubletsError { + NotExists(T), + LimitReached(T), + HasUsages(OwnedSlice>), + AlreadyExists(Doublet), + AllocFailed(Box), + Other(Box), +} + +impl DoubletsError { + #[cfg(unstable_backtrace)] + fn backtrace(&self) -> Option<&Backtrace> { + match self { + DoubletsError::AllocFailed(err) => err.request_ref(), + DoubletsError::Other(err) => err.request_ref(), + _ => None, + } + } +} + +impl Display for DoubletsError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + DoubletsError::NotExists(exists) => { + write!(f, "link {exists} does not exist.") + } + DoubletsError::LimitReached(limit) => { + write!(f, "links limit in storage has been reached: {limit}") + } + DoubletsError::HasUsages(usages) => { + write!(f, "link {usages:?} has dependencies") + } + DoubletsError::AlreadyExists(exists) => { + write!(f, "link {exists} already exists") + } + DoubletsError::AllocFailed(alloc) => { + write!(f, "alloc memory error: `{alloc}`") + } + DoubletsError::Other(other) => { + write!(f, "other internal error: `{other}`") + } + } + } +} + +use crate::utils::OwnedSlice; +use ffi_attributes as ffi; + +#[ffi::specialize_for( + types::( + u8 => u8, + u16 => u16, + u32 => u32, + u64 => u64, + ), + name = "doublets_free_error_*", + attributes( + #[no_mangle] + ) +)] +pub extern "C" fn free_error(err: DoubletsError) { + let _ = err; +} + +unsafe fn write_raw_msg(buf: *mut c_char, size: c_short, msg: &str) { + let cap = cmp::min(size as usize, msg.len()) - 1; + ptr::copy_nonoverlapping(msg.as_ptr(), buf.cast(), cap); + ptr::write(buf.add(cap), 0); +} + +#[ffi::specialize_for( + types::( + u8 => u8, + u16 => u16, + u32 => u32, + u64 => u64, + ), + name = "doublets_read_error_message_*", + attributes( + #[no_mangle] + ) +)] +pub unsafe extern "C" fn read_error( + buf: *mut c_char, + size: c_short, + error: &DoubletsError, +) { + write_raw_msg(buf, size, &error.to_string()); +} + +#[cfg(unstable_backtrace)] +#[ffi::specialize_for( + types::( + u8 => u8, + u16 => u16, + u32 => u32, + u64 => u64, + ), + name = "doublets_read_backtrace_*", + attributes( + #[no_mangle] + ) +)] +pub unsafe extern "C" fn read_backtrace( + buf: *mut c_char, + size: c_short, + error: &DoubletsError, +) { + if let Some(backtrace) = error.backtrace() { + write_raw_msg(buf, size, &backtrace.to_string()); + } +} diff --git a/doublets-ffi/src/export.rs b/doublets-ffi/src/export.rs new file mode 100644 index 0000000..bbe7970 --- /dev/null +++ b/doublets-ffi/src/export.rs @@ -0,0 +1,22 @@ +use crate::{ + logging::{DoubletsFFILogHandle, Format, Level, LogFFICallback}, + FFIContext, +}; + +#[no_mangle] +pub unsafe extern "C" fn doublets_create_log_handle( + ctx: FFIContext, + callback: LogFFICallback, + max_level: Level, + format: Format, + ansi: bool, +) -> Box { + Box::new(DoubletsFFILogHandle::new( + ctx, callback, max_level, format, ansi, + )) +} + +#[no_mangle] +pub unsafe extern "C" fn doublets_free_log_handle(log_handle: Box) { + let _ = log_handle; +} diff --git a/doublets-ffi/src/lib.rs b/doublets-ffi/src/lib.rs index c9c571a..dcd64b9 100644 --- a/doublets-ffi/src/lib.rs +++ b/doublets-ffi/src/lib.rs @@ -1,468 +1,33 @@ -#![feature(try_blocks)] -#![feature(box_syntax)] -#![feature(try_trait_v2)] +#![cfg_attr(unstable_backtrace, feature(error_generic_member_access))] -use std::{ - error::Error, - ffi::CStr, - fmt::Display, - fs::File, - mem, - ops::{RangeInclusive, Try}, - ptr::{drop_in_place, null_mut}, -}; +pub mod constants; +pub mod errors; +pub mod export; +pub mod logging; +pub mod store; +pub mod utils; -use doublets::{ - data::{ - query, - Flow::{Break, Continue}, - LinkType, LinksConstants, Query, ToQuery, - }, - mem::FileMapped, - Link, Links, -}; -use libc::c_char; -use log::{error, warn}; +pub(crate) use utils::stable_try; +// It is not useless: CLion highlight +// `c_char` as alias - italic +// `c_void` as type or alias - non-italic #[allow(non_camel_case_types)] -type c_void = core::ffi::c_void; - -use doublets::{parts, unit, Doublets}; -use ffi_attributes as ffi; - -fn result_into_log(result: Result, default: R) -> R { - result.unwrap_or_else(|e| { - error!("{e}"); - default - }) -} - -unsafe fn query_from_raw<'a, T: LinkType>(query: *const T, len: usize) -> Query<'a, T> { - // it not require `#[cfg(debug_assertions)]`, - // because it is used in debug log mode only (llvm optimization:)) - if query.is_null() && len != 0 { - warn!("if `query` is null then `len` must be 0"); - } - - if query.is_null() { - query![] - } else { - std::slice::from_raw_parts(query, len).to_query() - } -} - -unsafe fn unnull_or_error<'a, P, R>(ptr: *mut P) -> &'a mut R { - if ptr.is_null() { - // todo: use std::Backtrace or crates/tracing - error!("Null pointer"); - panic!("Null pointer"); - } else { - &mut *(ptr as *mut _) - } -} - -// TODO: remove ::mem:: in doublets crate -type UnitedLinks = unit::Store>>; - -type WrappedLinks = Box>; - -type EachCallback = extern "C" fn(Link) -> T; - -type CUDCallback = extern "C" fn(Link, Link) -> T; - -#[derive(Eq, PartialEq)] -#[repr(C)] -pub struct Range(pub T, pub T); - -#[repr(C)] -pub struct Constants { - pub index_part: T, - pub source_part: T, - pub target_part: T, - pub null: T, - pub r#continue: T, - pub r#break: T, - pub skip: T, - pub any: T, - pub itself: T, - pub error: T, - pub internal_range: Range, - pub external_range: Range, - pub _opt_marker: bool, -} - -impl From> for Constants { - fn from(c: LinksConstants) -> Self { - Self { - index_part: c.index_part, - source_part: c.source_part, - target_part: c.target_part, - null: c.null, - r#continue: c.r#continue, - r#break: c.r#break, - skip: c.skip, - any: c.any, - itself: c.itself, - error: c.error, - internal_range: Range(*c.internal_range.start(), *c.internal_range.end()), - // external_range: c.external_range.map(|r| Range(*r.start(), *r.end())), - external_range: c - .clone() - .external_range - .map_or(Range(T::funty(0), T::funty(0)), |r| { - Range(*r.start(), *r.end()) - }), - _opt_marker: c.external_range.is_some(), - } - } -} - -#[allow(clippy::from_over_into)] -impl Into> for Constants { - fn into(self) -> LinksConstants { - LinksConstants { - index_part: self.index_part, - source_part: self.source_part, - target_part: self.target_part, - r#break: self.r#break, - null: self.null, - r#continue: self.r#continue, - skip: self.skip, - any: self.any, - itself: self.itself, - error: self.error, - internal_range: RangeInclusive::new(self.internal_range.0, self.internal_range.1), - external_range: if self._opt_marker { - Some(RangeInclusive::new( - self.external_range.0, - self.external_range.1, - )) - } else { - None - }, - } - } -} - -#[ffi::specialize_for( - types = "u8", - types = "u16", - types = "u32", - types = "u64", - convention = "csharp", - name = "*Links_New" -)] -unsafe fn new_united_links(path: *const c_char) -> *mut c_void { - new_with_constants_united_links::(path, LinksConstants::external().into()) -} - -#[ffi::specialize_for( - types = "u8", - types = "u16", - types = "u32", - types = "u64", - convention = "csharp", - name = "*Links_NewWithConstants" -)] -unsafe fn new_with_constants_united_links( - path: *const c_char, - constants: Constants, -) -> *mut c_void { - let result: Result<_, Box> = try { - let path = CStr::from_ptr(path).to_str()?; - let file = File::options() - .create(true) - .read(true) - .write(true) - .open(path)?; - let mem = FileMapped::new(file)?; - let mut links: Box> = - box UnitedLinks::::with_constants(mem, constants.into())?; - let ptr = links.as_mut() as *mut _ as *mut c_void; - mem::forget(links); - ptr - }; - result_into_log(result, null_mut()) -} - -#[ffi::specialize_for( - types = "u8", - types = "u16", - types = "u32", - types = "u64", - convention = "csharp", - name = "*Links_Drop" -)] -unsafe fn drop_united_links(this: *mut c_void) { - let links: &mut WrappedLinks = unnull_or_error(this); - drop_in_place(links); -} - -#[ffi::specialize_for( - types = "u8", - types = "u16", - types = "u32", - types = "u64", - convention = "csharp", - name = "*Links_GetConstants" -)] -unsafe fn get_constants_united_links(this: *mut c_void) -> Constants { - let links: &mut WrappedLinks = unnull_or_error(this); - links.constants().clone().into() -} - -#[ffi::specialize_for( - types = "u8", - types = "u16", - types = "u32", - types = "u64", - convention = "csharp", - name = "*Links_Create" -)] -unsafe fn create_united( - this: *mut c_void, - query: *const T, - len: usize, - callback: CUDCallback, -) -> T { - let links: &mut WrappedLinks = unnull_or_error(this); - let continue_ = links.constants().r#continue; - let break_ = links.constants().r#break; - let result = { - let query = query_from_raw(query, len); - let handler = |before: Link<_>, after: Link<_>| { - if callback(before, after) == continue_ { - Break - } else { - Continue - } - }; - links.create_by_with(query, handler) - }; - result_into_log( - result.map(|flow| { - if flow.branch().is_continue() { - continue_ - } else { - break_ - } - }), - links.constants().error, - ) -} - -#[ffi::specialize_for( - types = "u8", - types = "u16", - types = "u32", - types = "u64", - convention = "csharp", - name = "*Links_SmartCreate" -)] -unsafe fn smart_create_united(this: *mut c_void) -> T { - let links: &mut WrappedLinks = unnull_or_error(this); - let result = links.create(); - result_into_log(result, links.constants().error) -} - -#[ffi::specialize_for( - types = "u8", - types = "u16", - types = "u32", - types = "u64", - convention = "csharp", - name = "*Links_SmartUpdate" -)] -unsafe fn smart_update_united(this: *mut c_void, index: T, source: T, target: T) -> T { - let links: &mut WrappedLinks = unnull_or_error(this); - let result = links.update(index, source, target); - result_into_log(result, links.constants().error) -} - -#[ffi::specialize_for( - types = "u8", - types = "u16", - types = "u32", - types = "u64", - convention = "csharp", - name = "*Links_Each" -)] -unsafe fn each_united( - this: *mut c_void, - query: *const T, - len: usize, - callback: EachCallback, -) -> T { - let links: &mut WrappedLinks = unnull_or_error(this); - let query = query_from_raw(query, len); - let r#continue = links.constants().r#continue; - let r#break = links.constants().r#break; - let result = links.each_by(query, move |link| { - if callback(link) == r#continue { - Continue - } else { - Break - } - }); - match result { - Continue => r#continue, - Break => r#break, - } -} - -#[ffi::specialize_for( - types = "u8", - types = "u16", - types = "u32", - types = "u64", - convention = "csharp", - name = "*Links_Count" -)] -unsafe fn count_united(this: *mut c_void, query: *const T, len: usize) -> T { - let links: &mut WrappedLinks = unnull_or_error(this); - let query = query_from_raw(query, len); - links.count_by(query) -} - -#[ffi::specialize_for( - types = "u8", - types = "u16", - types = "u32", - types = "u64", - convention = "csharp", - name = "*Links_Update" -)] -unsafe fn update_united( - this: *mut c_void, - restrictions: *const T, - len_r: usize, - substitutuion: *const T, - len_s: usize, - callback: CUDCallback, -) -> T { - let restrictions = query_from_raw(restrictions, len_r); - let substitutuion = query_from_raw(substitutuion, len_s); - let links: &mut WrappedLinks = unnull_or_error(this); - let continue_ = links.constants().r#continue; - let break_ = links.constants().r#break; - let result = { - let handler = move |before: Link, after: Link| { - if callback(before, after) == continue_ { - Continue - } else { - Break - } - }; - links.update_by_with(restrictions, substitutuion, handler) - }; - result_into_log( - result.map(|flow| { - if flow.branch().is_continue() { - continue_ - } else { - break_ - } - }), - links.constants().error, - ) -} - -#[ffi::specialize_for( - types = "u8", - types = "u16", - types = "u32", - types = "u64", - convention = "csharp", - name = "*Links_Delete" -)] -unsafe fn delete_united( - this: *mut c_void, - query: *const T, - len: usize, - callback: CUDCallback, -) -> T { - let query = query_from_raw(query, len); - let links: &mut WrappedLinks = unnull_or_error(this); - let continue_ = links.constants().r#continue; - let break_ = links.constants().r#break; - let result = { - let handler = move |before: Link<_>, after: Link<_>| { - if callback(before, after) == break_ { - Break - } else { - Continue - } - }; - links.delete_by_with(query, handler) - }; - result_into_log( - result.map(|flow| { - if flow.branch().is_continue() { - continue_ - } else { - break_ - } - }), - links.constants().error, - ) -} - -#[repr(C)] -pub struct SharedLogger { - formatter: for<'a> extern "C" fn(&'a log::Record<'_>), -} - -impl log::Log for SharedLogger { - fn enabled(&self, _: &log::Metadata) -> bool { - true - } - fn log(&self, record: &log::Record) { - (self.formatter)(record) - } - fn flush(&self) {} -} - -pub fn build_shared_logger() -> SharedLogger { - extern "C" fn formatter(r: &log::Record<'_>) { - tracing_log::format_trace(r).unwrap() - } - SharedLogger { formatter } -} - -#[no_mangle] -pub extern "C" fn setup_shared_logger(logger: SharedLogger) { - log::set_max_level(log::STATIC_MAX_LEVEL); - - let subscriber = tracing_subscriber::fmt::fmt() - .with_max_level(tracing::Level::TRACE) - .finish(); - if let Err(err) = tracing::subscriber::set_global_default(subscriber) { - warn!("subscriber error: {}", err) - } - - if let Err(err) = log::set_boxed_logger(Box::new(logger)) { - warn!("{}", err) - } -} - -#[no_mangle] -pub extern "C" fn init_fmt_logger() { - let logger = build_shared_logger(); - setup_shared_logger(logger); -} - -mod tests { - #[test] - fn error_log() { - use crate::{build_shared_logger, setup_shared_logger}; - use log::{debug, error, info, trace, warn}; - - let logger = build_shared_logger(); - setup_shared_logger(logger); - trace!("trace"); - debug!("debug"); - info!("info"); - warn!("warn"); - error!("error"); - } -} +type c_void = std::ffi::c_void; +#[allow(non_camel_case_types)] +type c_char = std::ffi::c_char; + +pub type FFIContext = *mut c_void; + +/// [`Send`] and [`Sync`] wrapper on [`FFICallbackContext`] +/// +/// WARNING: value of `FFICallbackContext` Context must be transferred across thread boundaries +/// and safe to share references between threads. +/// +/// Otherwise value not use in multithreading context +#[derive(Clone, Copy)] +pub struct FFIContextWrapper(FFIContext); + +/// Guarantee by caller side +unsafe impl Send for FFIContextWrapper {} +unsafe impl Sync for FFIContextWrapper {} diff --git a/doublets-ffi/src/logging.rs b/doublets-ffi/src/logging.rs new file mode 100644 index 0000000..fe2453e --- /dev/null +++ b/doublets-ffi/src/logging.rs @@ -0,0 +1,143 @@ +use super::{c_char, FFIContext}; +use crate::FFIContextWrapper; +use crossbeam_channel::{self as mpsc, Sender}; +use std::{ffi::CString, io, thread}; +use tap::Pipe; +use tracing::{error, subscriber, Subscriber}; +use tracing_subscriber::{ + filter::{EnvFilter, LevelFilter}, + fmt::MakeWriter, +}; + +struct ChannelWriter { + sender: Sender>, +} + +impl ChannelWriter { + pub fn new(sender: Sender>) -> Self { + Self { sender } + } +} + +impl io::Write for ChannelWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + let len = buf.len(); + let _ = self.sender.send(buf.to_vec()); + Ok(len) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl MakeWriter<'_> for ChannelWriter { + type Writer = ChannelWriter; + + fn make_writer(&self) -> Self::Writer { + ChannelWriter { + sender: self.sender.clone(), + } + } +} + +/// # Safety +/// This callback is safe if all the rules of Rust are followed +pub type LogFFICallback = unsafe extern "C" fn(FFIContext, *const c_char); + +#[repr(usize)] +pub enum Level { + Trace = 0, + Debug = 1, + Info = 2, + Warn = 3, + Error = 4, + Off = 5, +} + +#[repr(usize)] +pub enum Format { + Virgin, + Pretty, + Json, +} + +pub struct DoubletsFFILogHandle { + _non_exhaustive: (), +} + +impl DoubletsFFILogHandle { + pub fn new( + ctx: FFIContext, + callback: LogFFICallback, + max_level: Level, + format: Format, + ansi: bool, + ) -> Self { + log_panics::init(); + let wrapper = FFIContextWrapper(ctx); + let (sender, receiver) = mpsc::bounded(256); + + let callback = move |ctx: FFIContextWrapper, ptr| { + // SAFETY: caller must guarantee - we only delegate callback + unsafe { + callback(ctx.0, ptr); + } + }; + + thread::spawn(move || { + // We can use `while let Ok(msg) = receiver.recv()` + // `crossbeam::recv` is blocking it is base + + // info_span!("Logging loop").in_scope(|| { + while let Ok(msg) = receiver.recv() { + let str = + CString::new(msg).expect("Only UTF-8 format strings are allowed in logging"); + callback(wrapper, str.as_ptr()); + } + // }); + }); + + let filter = EnvFilter::from_default_env().add_directive( + match max_level { + Level::Trace => LevelFilter::TRACE, + Level::Debug => LevelFilter::DEBUG, + Level::Info => LevelFilter::INFO, + Level::Warn => LevelFilter::WARN, + Level::Error => LevelFilter::ERROR, + Level::Off => LevelFilter::OFF, + } + .into(), + ); + + macro_rules! subscribe { + ($($methods:tt)*) => { + tracing_subscriber::fmt() + $($methods)* + .with_ansi(ansi) + .with_writer(ChannelWriter::new(sender)) + .with_env_filter(filter) + .with_filter_reloading() + .finish() + }; + } + + if match format { + Format::Virgin => Box::new(subscribe!()) as Box, + Format::Pretty => Box::new(subscribe! { .pretty() }), + Format::Json => Box::new(subscribe! { .json() }), + } + .pipe(subscriber::set_global_default) + .is_err() + { + error!( + "Log handler already set, cannot currently change: track issue \ + `https://github.com/linksplatform/doublets-rs/issues/12`" + ); + }; + + Self { + _non_exhaustive: (), + } + } +} diff --git a/doublets-ffi/src/store.rs b/doublets-ffi/src/store.rs new file mode 100644 index 0000000..2340dba --- /dev/null +++ b/doublets-ffi/src/store.rs @@ -0,0 +1,354 @@ +#![allow(clippy::missing_safety_doc)] + +use crate::{ + c_char, + constants::Constants, + errors::DoubletsError, + stable_try as tri, + utils::{Fallible, Maybe, OwnedSlice}, + FFIContext, +}; +use doublets::{ + data::{Flow, LinkType}, + mem::FileMapped, + parts, unit, Doublets, Error, Link, Links, +}; +use ffi_attributes as ffi; +use std::{ + ffi::CStr, + fmt::{self, Debug, Formatter}, + slice, +}; +use tap::Pipe; +use tracing::{debug, warn}; + +type UnitedLinks = unit::Store>>; + +type EachCallback = extern "C" fn(FFIContext, Link) -> Flow; + +type CUDCallback = extern "C" fn(FFIContext, Link, Link) -> Flow; + +pub struct StoreHandle { + pointer: Box>, +} + +impl Debug for StoreHandle { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("StoreHandle") + .field("pointer", &(self.pointer.as_ref() as *const _)) + .finish() + } +} + +impl StoreHandle { + pub fn new(store: Box>) -> Self { + Self { pointer: store } + } + + pub unsafe fn assume(&mut self) -> &mut Box> { + &mut self.pointer + } + + #[allow(clippy::borrowed_box)] // needs for `Self: Sized` also can use `&impl Doublets` + pub unsafe fn assume_ref(&self) -> &Box> { + &self.pointer + } + + /// This function is actually unsafe + /// + /// # Safety + /// + /// Caller guarantee that will not drop handle + // fixme: may be we can port `result::Result` to C + pub fn invalid(err: Error) -> Maybe { + acquire_error(err); + + Maybe::none() + } +} + +unsafe fn thin_query_from_raw<'a, T>(query: *const T, len: u32) -> &'a [T] { + if query.is_null() { + &[] + } else { + slice::from_raw_parts(query, len as usize) + } +} + +unsafe fn query_from_raw<'a, T>(query: *const T, len: u32) -> &'a [T] { + if query.is_null() && len != 0 { + warn!("query ptr is null, but len is not null: handle could be a potential mistake."); + } + + thin_query_from_raw(query, len) +} + +impl From> for DoubletsError { + fn from(err: Error) -> Self { + match err { + Error::NotExists(link) => Self::NotExists(link), + Error::HasUsages(usages) => Self::HasUsages(OwnedSlice::leak(usages)), + Error::AlreadyExists(exists) => Self::AlreadyExists(exists), + Error::LimitReached(limit) => Self::LimitReached(limit), + // these errors are difficult to handle as data + // I hope no one will be offended if we alloc them at the heap + Error::AllocFailed(alloc) => Self::AllocFailed(Box::new(Box::new(alloc))), + Error::Other(other) => Self::Other(Box::new(other)), + } + } +} + +fn acquire_error(err: Error) -> DoubletsError { + // It can be very expensive to handle each error + debug!(op_error = % err); + err.into() +} + +fn acquire_result(result: Result>) -> Fallible> { + result.map_err(acquire_error).into() +} + +#[tracing::instrument( + skip_all, + fields( + path = ?CStr::from_ptr(path).to_str(), + path.ptr = ?path, + ), +)] +#[ffi::specialize_for( + types::( + u8 => u8, + u16 => u16, + u32 => u32, + u64 => u64, + ), + name = "doublets_create_united_store_*", + attributes( + #[no_mangle] + ) +)] +pub unsafe extern "C" fn create_unit_store( + path: *const c_char, + constants: Constants, +) -> Fallible>, DoubletsError> { + let result: Result<_, Error> = tri! { + let path = CStr::from_ptr(path).to_str().unwrap(); + let mem = FileMapped::from_path(path)?; + StoreHandle::new(Box::new(UnitedLinks::with_constants( + mem, + constants.into(), + )?)) + }; + result.map(Box::new).pipe(acquire_result) +} + +#[ffi::specialize_for( + types::( + u8 => u8, + u16 => u16, + u32 => u32, + u64 => u64, + ), + name = "doublets_free_store_*", + attributes( + #[no_mangle] + ) +)] +pub unsafe extern "C" fn free_store(handle: Box>) { + let _ = handle; +} + +#[ffi::specialize_for( + types::( + u8 => u8, + u16 => u16, + u32 => u32, + u64 => u64, + ), + name = "doublets_constants_*", + attributes( + #[no_mangle] + ) +)] +pub unsafe extern "C" fn constants_from_store( + handle: &StoreHandle, +) -> Constants { + handle + .assume_ref() + .constants() + .clone() // fixme: useless .clone + .into() +} + +#[tracing::instrument( + skip_all, + fields( + query = ?thin_query_from_raw(query, len), + query.ptr = ?query, + query.len = len, + ), +)] +#[ffi::specialize_for( + types::( + u8 => u8, + u16 => u16, + u32 => u32, + u64 => u64, + ), + name = "doublets_create_*", + attributes( + #[no_mangle] + ) +)] +pub unsafe extern "C" fn create( + handle: &mut StoreHandle, + query: *const T, + len: u32, + ctx: FFIContext, + callback: CUDCallback, +) -> Fallible> { + let query = query_from_raw(query, len); + let handler = move |before, after| callback(ctx, before, after); + handle + .assume() + .create_by_with(query, handler) + .pipe(acquire_result) +} + +#[tracing::instrument( + skip_all, + fields( + query = ?thin_query_from_raw(query, len), + query.ptr = ?query, + query.len = len, + ), +)] +#[ffi::specialize_for( + types::( + u8 => u8, + u16 => u16, + u32 => u32, + u64 => u64, + ), + name = "doublets_each_*", + attributes( + #[no_mangle] + ) +)] +pub unsafe extern "C" fn each( + handle: &StoreHandle, + query: *const T, + len: u32, + ctx: FFIContext, + callback: EachCallback, +) -> Flow { + let query = query_from_raw(query, len); + let handler = move |link| callback(ctx, link); + handle.assume_ref().each_by(query, handler) +} + +#[tracing::instrument( + skip_all, + fields( + query = ?thin_query_from_raw(query, len), + query.ptr = ?query, + query.len = len, + ), +)] +#[ffi::specialize_for( + types::( + u8 => u8, + u16 => u16, + u32 => u32, + u64 => u64, + ), + name = "doublets_count_*", + attributes( + #[no_mangle] + ) +)] +pub unsafe extern "C" fn count( + handle: &mut StoreHandle, + query: *const T, + len: u32, +) -> T { + let query = query_from_raw(query, len); + handle.assume().count_by(query) +} + +#[tracing::instrument( + skip_all, + fields( + query = ?thin_query_from_raw(query, len_q), + query.ptr = ?query, + query.len = len_q, + + change = ?thin_query_from_raw(query, len_q), + change.ptr = ?change, + change.len = len_c, + ), +)] +#[ffi::specialize_for( + types::( + u8 => u8, + u16 => u16, + u32 => u32, + u64 => u64, + ), + name = "doublets_update_*", + attributes( + #[no_mangle] + ) +)] +pub unsafe extern "C" fn update( + handle: &mut StoreHandle, + query: *const T, + len_q: u32, + change: *const T, + len_c: u32, + ctx: FFIContext, + callback: CUDCallback, +) -> Fallible> { + let handler = move |before, after| callback(ctx, before, after); + let query = query_from_raw(query, len_q); + let change = query_from_raw(change, len_c); + handle + .assume() + .update_by_with(query, change, handler) + .pipe(acquire_result) +} + +#[tracing::instrument( + skip_all, + fields( + query = ?thin_query_from_raw(query, len), + query.ptr = ?query, + query.len = len, + ) +)] +#[ffi::specialize_for( + types::( + u8 => u8, + u16 => u16, + u32 => u32, + u64 => u64, + ), + name = "doublets_delete_*", + attributes( + #[no_mangle] + ) +)] +pub unsafe extern "C" fn delete( + handle: &mut StoreHandle, + query: *const T, + len: u32, + ctx: FFIContext, + callback: CUDCallback, +) -> Fallible> { + let handler = move |before, after| callback(ctx, before, after); + let query = query_from_raw(query, len); + handle + .assume() + .delete_by_with(query, handler) + .pipe(acquire_result) +} diff --git a/doublets-ffi/src/utils.rs b/doublets-ffi/src/utils.rs new file mode 100644 index 0000000..cc2671d --- /dev/null +++ b/doublets-ffi/src/utils.rs @@ -0,0 +1,155 @@ +/// Stable `try` block only for `Result` +macro_rules! stable_try { + ($($block:tt)*) => { + (|| { + let ret = { $($block)* }; + core::result::Result::Ok(ret) + })() + }; +} + +pub(crate) use stable_try; +use std::{ + fmt::{self, Debug, Formatter}, + marker::PhantomData, + ptr::{self, NonNull}, +}; + +/// repr C type unit type +/// with `sizeof == 0` and `alignof == 1`: +#[repr(C)] +pub struct None { + _nonzero: [u8; 0], +} + +impl None { + pub fn new() -> Self { + Self { _nonzero: [] } + } +} + +impl Debug for None { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "None value") + } +} + +#[repr(C, usize)] +pub enum Fallible { + Ok(T), + Err(E), +} + +impl Fallible { + pub fn unwrap(self) -> T { + match self { + Self::Ok(val) => val, + Self::Err(err) => { + panic!("called `Fallible::unwrap()` on a `Err` value: {err:?}") + } + } + } +} + +pub type Maybe = Fallible; + +impl Maybe { + pub fn none() -> Self { + Self::Err(None::new()) + } +} + +impl From> for Fallible { + fn from(result: Result) -> Self { + match result { + Ok(ok) => Self::Ok(ok), + Err(err) => Self::Err(err), + } + } +} + +impl From> for Result { + fn from(fallible: Fallible) -> Self { + match fallible { + Fallible::Ok(ok) => Ok(ok), + Fallible::Err(err) => Err(err), + } + } +} + +impl From> for Maybe { + fn from(opt: Option) -> Self { + match opt { + Some(val) => Maybe::Ok(val), + None => Maybe::none(), + } + } +} + +impl From> for Option { + fn from(maybe: Maybe) -> Self { + if let Maybe::Ok(val) = maybe { + Some(val) + } else { + None + } + } +} + +/// `OwnedSlice` is a FFI-Safe `Box<[T]>` representation +#[repr(C)] +pub struct OwnedSlice { + ptr: NonNull, + len: usize, + // actually it's still a `Box<[T]>` + _marker: PhantomData>, +} + +impl OwnedSlice { + #[inline] + pub fn slice_from_raw_parts(data: NonNull, len: usize) -> NonNull<[T]> { + // SAFETY: `data` is a `NonNull` pointer which is necessarily non-null + unsafe { NonNull::new_unchecked(ptr::slice_from_raw_parts_mut(data.as_ptr(), len)) } + } + + pub fn leak(place: Box<[T]>) -> Self { + let leak = NonNull::from(Box::leak(place)); + OwnedSlice { + // ptr: leak.as_non_null_ptr(), + ptr: leak.cast(), + len: leak.len(), + _marker: PhantomData, + } + } + + pub fn as_slice(&self) -> &[T] { + let slice = Self::slice_from_raw_parts(self.ptr, self.len); + // SAFETY: `Self` is opaque we create Box and we drop it + unsafe { slice.as_ref() } + } + + /// # Safety + /// forget `self` after `.keep_own` + pub unsafe fn keep_own(&self) -> Box<[T]> { + let slice = Self::slice_from_raw_parts(self.ptr, self.len); + unsafe { Box::from_raw(slice.as_ptr()) } + } + + pub fn into_owned(self) -> Box<[T]> { + // SAFETY: `self` drop after call `.into_owned` + unsafe { self.keep_own() } + } +} + +impl Debug for OwnedSlice { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.as_slice()) + } +} + +impl Drop for OwnedSlice { + fn drop(&mut self) { + // SAFETY: `self` drop at end of this scope + let _ = unsafe { self.keep_own() }; + } +} diff --git a/doublets/src/data/doublet.rs b/doublets/src/data/doublet.rs index 2a5d39b..7286842 100644 --- a/doublets/src/data/doublet.rs +++ b/doublets/src/data/doublet.rs @@ -3,6 +3,7 @@ use std::fmt::{Debug, Display, Formatter}; use data::LinkType; #[derive(Debug, Eq, PartialEq, Hash, Clone)] +#[repr(C)] pub struct Doublet { pub source: T, pub target: T, diff --git a/doublets/src/data/error.rs b/doublets/src/data/error.rs index 5cb0659..37b0195 100644 --- a/doublets/src/data/error.rs +++ b/doublets/src/data/error.rs @@ -8,7 +8,7 @@ pub enum Error { NotExists(T), #[error("link {0:?} has dependencies")] - HasUsages(Vec>), + HasUsages(Box<[Link]>), #[error("link {0} already exists")] AlreadyExists(Doublet),