Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

C Extension API #381

Merged
merged 42 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
eeca07d
PoC for a C EXTENSION API based extension
samansmink Aug 13, 2024
0e09e86
fix paths
0xcaff Aug 16, 2024
45e301f
use workspace versions
0xcaff Aug 16, 2024
df030a1
run cargo fmt
0xcaff Aug 16, 2024
7b046ca
fix submodule import
0xcaff Aug 16, 2024
fec044d
update bundled build code
0xcaff Aug 16, 2024
5d963e3
add updated generated assets
0xcaff Aug 16, 2024
ece9daf
remove bash -e
0xcaff Aug 16, 2024
29231f3
remove duplicate code
0xcaff Aug 16, 2024
c251b19
remove LIBDUCKDB_SYS_BUNDLING
0xcaff Aug 16, 2024
6b237da
split build and test
0xcaff Aug 16, 2024
30ff005
add todo
0xcaff Aug 16, 2024
d6a9ffc
remove update none
0xcaff Aug 16, 2024
424312a
add updated bundled bindgen
0xcaff Aug 16, 2024
f07a11d
Merge pull request #1 from 0xcaff/martin/sam-bundled-build
samansmink Aug 27, 2024
c8d93c4
wip
samansmink Aug 27, 2024
54e2618
bump duckdb, fix tests, fix loadable extension bindings
samansmink Sep 12, 2024
7621ce5
fix build, regen bindings
samansmink Sep 16, 2024
1a0089c
Merge branch 'main' into poc-rust-c-extension-api
samansmink Sep 16, 2024
cf9555c
format
samansmink Sep 16, 2024
81fe50c
remove makefile target for now
samansmink Sep 16, 2024
d8e47ab
apply proposed fix by @0xcaff for incorrect entrypoint signature
samansmink Sep 16, 2024
0bc3647
remove commented out lines of code
samansmink Sep 16, 2024
f333e73
bump duckdb, handle errors during initialization
samansmink Oct 4, 2024
147af76
format
samansmink Oct 4, 2024
07f522d
make clippy happy
samansmink Oct 4, 2024
6899dc2
add building the loadable capi extension to ci
samansmink Oct 4, 2024
1243fce
fix for polars changes api
samansmink Oct 4, 2024
488e769
workaround issue with polars and clippy
samansmink Oct 4, 2024
3f4297f
fix incompatiblity with newer polars version
samansmink Oct 4, 2024
a482097
fix small test breakage
samansmink Oct 4, 2024
260750f
workaround or windows too
samansmink Oct 4, 2024
27c503c
add feature dependency for example
samansmink Oct 4, 2024
eff7f8e
rerun bindgen using ubuntu
samansmink Oct 7, 2024
38286cc
make feature name consistent
samansmink Oct 7, 2024
a52d673
use space separated param
samansmink Oct 7, 2024
776d0ae
fix incorrect feature name here
samansmink Oct 7, 2024
0034648
remove incorrect required feature
samansmink Oct 7, 2024
1a96802
add warnings add correct feature dependency
samansmink Oct 7, 2024
ae3c1d3
downgrade polars again, due to tarpaulin issue
samansmink Oct 7, 2024
1743632
disable polars in asan for now
samansmink Oct 7, 2024
b2f5bf8
try skipping the proc macro package in asan job
samansmink Oct 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions .github/workflows/rust.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ jobs:
rust-version: stable${{ matrix.host }}
targets: ${{ matrix.target }}
components: 'rustfmt, clippy'

# download libduckdb
- uses: robinraju/[email protected]
name: Download duckdb
with:
repository: "duckdb/duckdb"
tag: "v1.0.0"
tag: "v1.1.1"
fileName: ${{ matrix.duckdb }}
out-file-path: .

Expand All @@ -49,15 +50,25 @@ jobs:
with:
file_path: ${{ github.workspace }}/${{ matrix.duckdb }}
extract_dir: libduckdb

- run: cargo fmt --all -- --check
if: matrix.os == 'ubuntu-latest'
- run: cargo clippy --all-targets --workspace --all-features -- -D warnings -A clippy::redundant-closure

# TODO: remove
- name: Workaround for https://github.com/pola-rs/polars/issues/19063
run: |
cargo update [email protected] --precise 2.5.0

- name: run cargo clippy
if: matrix.os == 'ubuntu-latest'
name: run cargo clippy
env:
DUCKDB_LIB_DIR: ${{ github.workspace }}/libduckdb
DUCKDB_INCLUDE_DIR: ${{ github.workspace }}/libduckdb
LD_LIBRARY_PATH: ${{ github.workspace }}/libduckdb
run: |
cargo clippy --all-targets --workspace --all-features -- -D warnings -A clippy::redundant-closure


- name: Run cargo-tarpaulin
if: matrix.os == 'ubuntu-latest'
uses: actions-rs/[email protected]
Expand All @@ -70,6 +81,7 @@ jobs:
DUCKDB_LIB_DIR: ${{ github.workspace }}/libduckdb
DUCKDB_INCLUDE_DIR: ${{ github.workspace }}/libduckdb
LD_LIBRARY_PATH: ${{ github.workspace }}/libduckdb

- name: Upload to codecov.io
if: matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v1
Expand All @@ -88,19 +100,28 @@ jobs:
with:
name: PATH
value: $env:PATH;${{ github.workspace }}/libduckdb

- name: Run cargo-test
if: matrix.os == 'windows-latest'
run: cargo test --features "modern-full vtab-full vtab-loadable"
env:
DUCKDB_LIB_DIR: ${{ github.workspace }}/libduckdb
DUCKDB_INCLUDE_DIR: ${{ github.workspace }}/libduckdb

- name: Build loadable extension
run: cargo build --example hello-ext --features="vtab-loadable"
env:
DUCKDB_LIB_DIR: ${{ github.workspace }}/libduckdb
DUCKDB_INCLUDE_DIR: ${{ github.workspace }}/libduckdb
LD_LIBRARY_PATH: ${{ github.workspace }}/libduckdb

- name: Build loadable extension
run: cargo build --example hello-ext-capi --features="vtab-loadable loadable-extension"
env:
DUCKDB_LIB_DIR: ${{ github.workspace }}/libduckdb
DUCKDB_INCLUDE_DIR: ${{ github.workspace }}/libduckdb
LD_LIBRARY_PATH: ${{ github.workspace }}/libduckdb

Windows:
name: Windows build from source
needs: test
Expand All @@ -117,6 +138,7 @@ jobs:
with:
rust-version: stable
targets: x86_64-pc-windows-msvc

- run: cargo install cargo-examples

Sanitizer:
Expand All @@ -140,7 +162,9 @@ jobs:
# leak sanitization, but we don't care about backtraces here, so long
# as the other tests have them.
RUST_BACKTRACE: "0"
run: cargo -Z build-std test --features "modern-full extensions-full" --target x86_64-unknown-linux-gnu
run: |
# TODO switch back to modern-full once polars is fixed
cargo -Z build-std test --features "chrono serde_json url r2d2 uuid extensions-full" --target x86_64-unknown-linux-gnu --package duckdb
- name: publish crates --dry-run
uses: katyo/publish-crates@v2
with:
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ Cargo.lock

*.db

crates/libduckdb-sys/duckdb-sources/
crates/libduckdb-sys/duckdb-sources/*
crates/libduckdb-sys/duckdb/
crates/libduckdb-sys/._duckdb
1 change: 0 additions & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
[submodule "crates/libduckdb-sys/duckdb-sources"]
path = crates/libduckdb-sys/duckdb-sources
url = https://github.com/duckdb/duckdb
update = none
7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ members = [
]

[workspace.package]
version = "1.0.0"
version = "1.1.1"
authors = ["wangfenjin <[email protected]>"]
edition = "2021"
repository = "https://github.com/duckdb/duckdb-rs"
Expand All @@ -19,8 +19,8 @@ license = "MIT"
categories = ["database"]

[workspace.dependencies]
duckdb = { version = "1.0.0", path = "crates/duckdb" }
libduckdb-sys = { version = "1.0.0", path = "crates/libduckdb-sys" }
duckdb = { version = "1.1.1", path = "crates/duckdb" }
libduckdb-sys = { version = "1.1.1", path = "crates/libduckdb-sys" }
duckdb-loadable-macros = { version = "0.1.2", path = "crates/duckdb-loadable-macros" }
autocfg = "1.0"
bindgen = { version = "0.69", default-features = false }
Expand All @@ -43,6 +43,7 @@ pkg-config = "0.3.24"
polars = "0.35.4"
polars-core = "0.35.4"
pretty_assertions = "1.4.0"
prettyplease = "0.2.20"
proc-macro2 = "1.0.56"
quote = "1.0.21"
r2d2 = "0.8.9"
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ all:
cargo clippy --all-targets --workspace --features buildtime_bindgen --features modern-full -- -D warnings -A clippy::redundant-closure

test:
cargo test --features bundled --features modern-full -- --nocapture
cargo test --features bundled --features modern-full -- --nocapture
2 changes: 1 addition & 1 deletion add_rustfmt_hook.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ command -v rustfmt >/dev/null 2>&1 || { echo >&2 "Rustfmt is required but it's n
# write a whole script to pre-commit hook
# NOTE: it will overwrite pre-commit file!
cat > .git/hooks/pre-commit <<'EOF'
#!/bin/bash -e
#!/bin/bash
declare -a rust_files=()
files=$(git diff-index --name-only --cached HEAD)
echo 'Formatting source files'
Expand Down
1 change: 1 addition & 0 deletions crates/duckdb-loadable-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ description = "Native bindings to the libduckdb library, C API; build loadable e
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
darling = "0.20.10"
proc-macro2 = { workspace = true }
quote = { workspace = true }
syn = { workspace = true, features = ["extra-traits", "full", "fold", "parsing"] }
Expand Down
102 changes: 102 additions & 0 deletions crates/duckdb-loadable-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,108 @@ use syn::{parse_macro_input, spanned::Spanned, Item};
use proc_macro::TokenStream;
use quote::quote_spanned;

use darling::{ast::NestedMeta, Error, FromMeta};

/// For parsing the arguments to the duckdb_entrypoint_c_api macro
#[derive(Debug, FromMeta)]
struct CEntryPointMacroArgs {
#[darling(default)]
/// The name to be given to this extension. This name is used in the entrypoint function called by duckdb
ext_name: String,
/// The minimum C API version this extension requires. It is recommended to set this to the lowest possible version
/// at which your extension still compiles
min_duckdb_version: Option<String>,
}

/// Wraps an entrypoint function to expose an unsafe extern "C" function of the same name.
/// Warning: experimental!
#[proc_macro_attribute]
pub fn duckdb_entrypoint_c_api(attr: TokenStream, item: TokenStream) -> TokenStream {
let attr_args = match NestedMeta::parse_meta_list(attr.into()) {
Ok(v) => v,
Err(e) => {
return TokenStream::from(Error::from(e).write_errors());
}
};

let args = match CEntryPointMacroArgs::from_list(&attr_args) {
Ok(v) => v,
Err(e) => {
return TokenStream::from(e.write_errors());
}
};

// Set the minimum duckdb version (dev by default)
let minimum_duckdb_version = match args.min_duckdb_version {
Some(i) => i,
None => "dev".to_string(),
};

let ast = parse_macro_input!(item as syn::Item);

match ast {
Item::Fn(func) => {
let c_entrypoint = Ident::new(format!("{}_init_c_api", args.ext_name).as_str(), Span::call_site());
let prefixed_original_function = func.sig.ident.clone();
let c_entrypoint_internal = Ident::new(
format!("{}_init_c_api_internal", args.ext_name).as_str(),
Span::call_site(),
);

quote_spanned! {func.span()=>
/// # Safety
///
/// Internal Entrypoint for error handling
pub unsafe fn #c_entrypoint_internal(info: ffi::duckdb_extension_info, access: *const ffi::duckdb_extension_access) -> Result<bool, Box<dyn std::error::Error>> {
let have_api_struct = ffi::duckdb_rs_extension_api_init(info, access, #minimum_duckdb_version).unwrap();

if !have_api_struct {
// initialization failed to return an api struct, likely due to an API version mismatch, we can simply return here
return Ok(false);
}

// TODO: handle error here?
let db : ffi::duckdb_database = *(*access).get_database.unwrap()(info);
let connection = Connection::open_from_raw(db.cast())?;

#prefixed_original_function(connection)?;

Ok(true)
}

/// # Safety
///
/// Entrypoint that will be called by DuckDB
#[no_mangle]
pub unsafe extern "C" fn #c_entrypoint(info: ffi::duckdb_extension_info, access: *const ffi::duckdb_extension_access) -> bool {
let init_result = #c_entrypoint_internal(info, access);

if let Err(x) = init_result {
let error_c_string = std::ffi::CString::new(x.to_string());

match error_c_string {
Ok(e) => {
(*access).set_error.unwrap()(info, e.as_ptr());
},
Err(_e) => {
let error_alloc_failure = c"An error occured but the extension failed to allocate memory for an error string";
(*access).set_error.unwrap()(info, error_alloc_failure.as_ptr());
}
}
return false;
}

init_result.unwrap()
}

#func
}
.into()
}
_ => panic!("Only function items are allowed on duckdb_entrypoint"),
}
}

/// Wraps an entrypoint function to expose an unsafe extern "C" function of the same name.
#[proc_macro_attribute]
pub fn duckdb_entrypoint(_attr: TokenStream, item: TokenStream) -> TokenStream {
Expand Down
9 changes: 8 additions & 1 deletion crates/duckdb/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "duckdb"
version = "1.0.0"
version = "1.1.1"
authors.workspace = true
edition.workspace = true
repository.workspace = true
Expand Down Expand Up @@ -35,6 +35,8 @@ polars = ["dep:polars"]
# FIXME: These were added to make clippy happy: these features appear unused and should perhaps be removed
column_decltype = []
extra_check = []
# Warning: experimental feature
loadable-extension = ["libduckdb-sys/loadable-extension"]

[dependencies]
libduckdb-sys = { workspace = true }
Expand Down Expand Up @@ -93,3 +95,8 @@ all-features = false
name = "hello-ext"
crate-type = ["cdylib"]
required-features = ["vtab-loadable"]

[[example]]
name = "hello-ext-capi"
crate-type = ["cdylib"]
required-features = ["vtab-loadable", "loadable-extension"]
93 changes: 93 additions & 0 deletions crates/duckdb/examples/hello-ext-capi/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
extern crate duckdb;
extern crate duckdb_loadable_macros;
extern crate libduckdb_sys;

use duckdb::{
core::{DataChunkHandle, Inserter, LogicalTypeHandle, LogicalTypeId},
vtab::{BindInfo, Free, FunctionInfo, InitInfo, VTab},
Connection, Result,
};
use duckdb_loadable_macros::duckdb_entrypoint_c_api;
use libduckdb_sys as ffi;
use std::{
error::Error,
ffi::{c_char, CString},
};

#[repr(C)]
struct HelloBindData {
name: *mut c_char,
}

impl Free for HelloBindData {
fn free(&mut self) {
unsafe {
if self.name.is_null() {
samansmink marked this conversation as resolved.
Show resolved Hide resolved
return;
}
drop(CString::from_raw(self.name));
}
}
}

#[repr(C)]
struct HelloInitData {
done: bool,
}

struct HelloVTab;

impl Free for HelloInitData {}

impl VTab for HelloVTab {
type InitData = HelloInitData;
type BindData = HelloBindData;

unsafe fn bind(bind: &BindInfo, data: *mut HelloBindData) -> Result<(), Box<dyn std::error::Error>> {
bind.add_result_column("column0", LogicalTypeHandle::from(LogicalTypeId::Varchar));
let param = bind.get_parameter(0).to_string();
unsafe {
(*data).name = CString::new(param).unwrap().into_raw();
}
Ok(())
}

unsafe fn init(_: &InitInfo, data: *mut HelloInitData) -> Result<(), Box<dyn std::error::Error>> {
unsafe {
(*data).done = false;
}
Ok(())
}

unsafe fn func(func: &FunctionInfo, output: &mut DataChunkHandle) -> Result<(), Box<dyn std::error::Error>> {
let init_info = func.get_init_data::<HelloInitData>();
let bind_info = func.get_bind_data::<HelloBindData>();

unsafe {
if (*init_info).done {
output.set_len(0);
} else {
(*init_info).done = true;
let vector = output.flat_vector(0);
let name = CString::from_raw((*bind_info).name);
let result = CString::new(format!("Hello {}", name.to_str()?))?;
// Can't consume the CString
(*bind_info).name = CString::into_raw(name);
vector.insert(0, result);
output.set_len(1);
}
}
Ok(())
}

fn parameters() -> Option<Vec<LogicalTypeHandle>> {
Some(vec![LogicalTypeHandle::from(LogicalTypeId::Varchar)])
}
}

#[duckdb_entrypoint_c_api(ext_name = "rusty_quack", min_duckdb_version = "v0.0.1")]
pub fn extension_entrypoint(con: Connection) -> Result<(), Box<dyn Error>> {
con.register_table_function::<HelloVTab>("hello")
.expect("Failed to register hello table function");
Ok(())
}
Loading
Loading