Skip to content

Commit 71b01f7

Browse files
samansmink0xcaff
andauthored
C Extension API (#381)
Initial experimental(!) implementation of loadable extensions with C extension API

See duckdb/duckdb#12682 for more info on the DuckDB C extension API --------- Co-authored-by: martin <[email protected]>
1 parent f887844 commit 71b01f7

25 files changed

+19839
-123
lines changed

.github/workflows/rust.yaml

+28-4
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,13 @@ jobs:
3333
rust-version: stable${{ matrix.host }}
3434
targets: ${{ matrix.target }}
3535
components: 'rustfmt, clippy'
36+
3637
# download libduckdb
3738
- uses: robinraju/[email protected]
3839
name: Download duckdb
3940
with:
4041
repository: "duckdb/duckdb"
41-
tag: "v1.0.0"
42+
tag: "v1.1.1"
4243
fileName: ${{ matrix.duckdb }}
4344
out-file-path: .
4445

@@ -49,15 +50,25 @@ jobs:
4950
with:
5051
file_path: ${{ github.workspace }}/${{ matrix.duckdb }}
5152
extract_dir: libduckdb
53+
5254
- run: cargo fmt --all -- --check
5355
if: matrix.os == 'ubuntu-latest'
54-
- run: cargo clippy --all-targets --workspace --all-features -- -D warnings -A clippy::redundant-closure
56+
57+
# TODO: remove
58+
- name: Workaround for https://github.com/pola-rs/polars/issues/19063
59+
run: |
60+
cargo update [email protected] --precise 2.5.0
61+
62+
- name: run cargo clippy
5563
if: matrix.os == 'ubuntu-latest'
56-
name: run cargo clippy
5764
env:
5865
DUCKDB_LIB_DIR: ${{ github.workspace }}/libduckdb
5966
DUCKDB_INCLUDE_DIR: ${{ github.workspace }}/libduckdb
6067
LD_LIBRARY_PATH: ${{ github.workspace }}/libduckdb
68+
run: |
69+
cargo clippy --all-targets --workspace --all-features -- -D warnings -A clippy::redundant-closure
70+
71+
6172
- name: Run cargo-tarpaulin
6273
if: matrix.os == 'ubuntu-latest'
6374
uses: actions-rs/[email protected]
@@ -70,6 +81,7 @@ jobs:
7081
DUCKDB_LIB_DIR: ${{ github.workspace }}/libduckdb
7182
DUCKDB_INCLUDE_DIR: ${{ github.workspace }}/libduckdb
7283
LD_LIBRARY_PATH: ${{ github.workspace }}/libduckdb
84+
7385
- name: Upload to codecov.io
7486
if: matrix.os == 'ubuntu-latest'
7587
uses: codecov/codecov-action@v1
@@ -88,19 +100,28 @@ jobs:
88100
with:
89101
name: PATH
90102
value: $env:PATH;${{ github.workspace }}/libduckdb
103+
91104
- name: Run cargo-test
92105
if: matrix.os == 'windows-latest'
93106
run: cargo test --features "modern-full vtab-full vtab-loadable"
94107
env:
95108
DUCKDB_LIB_DIR: ${{ github.workspace }}/libduckdb
96109
DUCKDB_INCLUDE_DIR: ${{ github.workspace }}/libduckdb
110+
97111
- name: Build loadable extension
98112
run: cargo build --example hello-ext --features="vtab-loadable"
99113
env:
100114
DUCKDB_LIB_DIR: ${{ github.workspace }}/libduckdb
101115
DUCKDB_INCLUDE_DIR: ${{ github.workspace }}/libduckdb
102116
LD_LIBRARY_PATH: ${{ github.workspace }}/libduckdb
103117

118+
- name: Build loadable extension
119+
run: cargo build --example hello-ext-capi --features="vtab-loadable loadable-extension"
120+
env:
121+
DUCKDB_LIB_DIR: ${{ github.workspace }}/libduckdb
122+
DUCKDB_INCLUDE_DIR: ${{ github.workspace }}/libduckdb
123+
LD_LIBRARY_PATH: ${{ github.workspace }}/libduckdb
124+
104125
Windows:
105126
name: Windows build from source
106127
needs: test
@@ -117,6 +138,7 @@ jobs:
117138
with:
118139
rust-version: stable
119140
targets: x86_64-pc-windows-msvc
141+
120142
- run: cargo install cargo-examples
121143

122144
Sanitizer:
@@ -140,7 +162,9 @@ jobs:
140162
# leak sanitization, but we don't care about backtraces here, so long
141163
# as the other tests have them.
142164
RUST_BACKTRACE: "0"
143-
run: cargo -Z build-std test --features "modern-full extensions-full" --target x86_64-unknown-linux-gnu
165+
run: |
166+
# TODO switch back to modern-full once polars is fixed
167+
cargo -Z build-std test --features "chrono serde_json url r2d2 uuid extensions-full" --target x86_64-unknown-linux-gnu --package duckdb
144168
- name: publish crates --dry-run
145169
uses: katyo/publish-crates@v2
146170
with:

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@ Cargo.lock
2727

2828
*.db
2929

30-
crates/libduckdb-sys/duckdb-sources/
30+
crates/libduckdb-sys/duckdb-sources/*
3131
crates/libduckdb-sys/duckdb/
3232
crates/libduckdb-sys/._duckdb

.gitmodules

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
[submodule "crates/libduckdb-sys/duckdb-sources"]
22
path = crates/libduckdb-sys/duckdb-sources
33
url = https://github.com/duckdb/duckdb
4-
update = none

Cargo.toml

+4-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ members = [
77
]
88

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

2121
[workspace.dependencies]
22-
duckdb = { version = "1.0.0", path = "crates/duckdb" }
23-
libduckdb-sys = { version = "1.0.0", path = "crates/libduckdb-sys" }
22+
duckdb = { version = "1.1.1", path = "crates/duckdb" }
23+
libduckdb-sys = { version = "1.1.1", path = "crates/libduckdb-sys" }
2424
duckdb-loadable-macros = { version = "0.1.2", path = "crates/duckdb-loadable-macros" }
2525
autocfg = "1.0"
2626
bindgen = { version = "0.69", default-features = false }
@@ -43,6 +43,7 @@ pkg-config = "0.3.24"
4343
polars = "0.35.4"
4444
polars-core = "0.35.4"
4545
pretty_assertions = "1.4.0"
46+
prettyplease = "0.2.20"
4647
proc-macro2 = "1.0.56"
4748
quote = "1.0.21"
4849
r2d2 = "0.8.9"

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ all:
1111
cargo clippy --all-targets --workspace --features buildtime_bindgen --features modern-full -- -D warnings -A clippy::redundant-closure
1212

1313
test:
14-
cargo test --features bundled --features modern-full -- --nocapture
14+
cargo test --features bundled --features modern-full -- --nocapture

add_rustfmt_hook.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ command -v rustfmt >/dev/null 2>&1 || { echo >&2 "Rustfmt is required but it's n
88
# write a whole script to pre-commit hook
99
# NOTE: it will overwrite pre-commit file!
1010
cat > .git/hooks/pre-commit <<'EOF'
11-
#!/bin/bash -e
11+
#!/bin/bash
1212
declare -a rust_files=()
1313
files=$(git diff-index --name-only --cached HEAD)
1414
echo 'Formatting source files'

crates/duckdb-loadable-macros/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ description = "Native bindings to the libduckdb library, C API; build loadable e
1414
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1515

1616
[dependencies]
17+
darling = "0.20.10"
1718
proc-macro2 = { workspace = true }
1819
quote = { workspace = true }
1920
syn = { workspace = true, features = ["extra-traits", "full", "fold", "parsing"] }

crates/duckdb-loadable-macros/src/lib.rs

+102
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,108 @@ use syn::{parse_macro_input, spanned::Spanned, Item};
66
use proc_macro::TokenStream;
77
use quote::quote_spanned;
88

9+
use darling::{ast::NestedMeta, Error, FromMeta};
10+
11+
/// For parsing the arguments to the duckdb_entrypoint_c_api macro
12+
#[derive(Debug, FromMeta)]
13+
struct CEntryPointMacroArgs {
14+
#[darling(default)]
15+
/// The name to be given to this extension. This name is used in the entrypoint function called by duckdb
16+
ext_name: String,
17+
/// The minimum C API version this extension requires. It is recommended to set this to the lowest possible version
18+
/// at which your extension still compiles
19+
min_duckdb_version: Option<String>,
20+
}
21+
22+
/// Wraps an entrypoint function to expose an unsafe extern "C" function of the same name.
23+
/// Warning: experimental!
24+
#[proc_macro_attribute]
25+
pub fn duckdb_entrypoint_c_api(attr: TokenStream, item: TokenStream) -> TokenStream {
26+
let attr_args = match NestedMeta::parse_meta_list(attr.into()) {
27+
Ok(v) => v,
28+
Err(e) => {
29+
return TokenStream::from(Error::from(e).write_errors());
30+
}
31+
};
32+
33+
let args = match CEntryPointMacroArgs::from_list(&attr_args) {
34+
Ok(v) => v,
35+
Err(e) => {
36+
return TokenStream::from(e.write_errors());
37+
}
38+
};
39+
40+
// Set the minimum duckdb version (dev by default)
41+
let minimum_duckdb_version = match args.min_duckdb_version {
42+
Some(i) => i,
43+
None => "dev".to_string(),
44+
};
45+
46+
let ast = parse_macro_input!(item as syn::Item);
47+
48+
match ast {
49+
Item::Fn(func) => {
50+
let c_entrypoint = Ident::new(format!("{}_init_c_api", args.ext_name).as_str(), Span::call_site());
51+
let prefixed_original_function = func.sig.ident.clone();
52+
let c_entrypoint_internal = Ident::new(
53+
format!("{}_init_c_api_internal", args.ext_name).as_str(),
54+
Span::call_site(),
55+
);
56+
57+
quote_spanned! {func.span()=>
58+
/// # Safety
59+
///
60+
/// Internal Entrypoint for error handling
61+
pub unsafe fn #c_entrypoint_internal(info: ffi::duckdb_extension_info, access: *const ffi::duckdb_extension_access) -> Result<bool, Box<dyn std::error::Error>> {
62+
let have_api_struct = ffi::duckdb_rs_extension_api_init(info, access, #minimum_duckdb_version).unwrap();
63+
64+
if !have_api_struct {
65+
// initialization failed to return an api struct, likely due to an API version mismatch, we can simply return here
66+
return Ok(false);
67+
}
68+
69+
// TODO: handle error here?
70+
let db : ffi::duckdb_database = *(*access).get_database.unwrap()(info);
71+
let connection = Connection::open_from_raw(db.cast())?;
72+
73+
#prefixed_original_function(connection)?;
74+
75+
Ok(true)
76+
}
77+
78+
/// # Safety
79+
///
80+
/// Entrypoint that will be called by DuckDB
81+
#[no_mangle]
82+
pub unsafe extern "C" fn #c_entrypoint(info: ffi::duckdb_extension_info, access: *const ffi::duckdb_extension_access) -> bool {
83+
let init_result = #c_entrypoint_internal(info, access);
84+
85+
if let Err(x) = init_result {
86+
let error_c_string = std::ffi::CString::new(x.to_string());
87+
88+
match error_c_string {
89+
Ok(e) => {
90+
(*access).set_error.unwrap()(info, e.as_ptr());
91+
},
92+
Err(_e) => {
93+
let error_alloc_failure = c"An error occured but the extension failed to allocate memory for an error string";
94+
(*access).set_error.unwrap()(info, error_alloc_failure.as_ptr());
95+
}
96+
}
97+
return false;
98+
}
99+
100+
init_result.unwrap()
101+
}
102+
103+
#func
104+
}
105+
.into()
106+
}
107+
_ => panic!("Only function items are allowed on duckdb_entrypoint"),
108+
}
109+
}
110+
9111
/// Wraps an entrypoint function to expose an unsafe extern "C" function of the same name.
10112
#[proc_macro_attribute]
11113
pub fn duckdb_entrypoint(_attr: TokenStream, item: TokenStream) -> TokenStream {

crates/duckdb/Cargo.toml

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "duckdb"
3-
version = "1.0.0"
3+
version = "1.1.1"
44
authors.workspace = true
55
edition.workspace = true
66
repository.workspace = true
@@ -35,6 +35,8 @@ polars = ["dep:polars"]
3535
# FIXME: These were added to make clippy happy: these features appear unused and should perhaps be removed
3636
column_decltype = []
3737
extra_check = []
38+
# Warning: experimental feature
39+
loadable-extension = ["libduckdb-sys/loadable-extension"]
3840

3941
[dependencies]
4042
libduckdb-sys = { workspace = true }
@@ -93,3 +95,8 @@ all-features = false
9395
name = "hello-ext"
9496
crate-type = ["cdylib"]
9597
required-features = ["vtab-loadable"]
98+
99+
[[example]]
100+
name = "hello-ext-capi"
101+
crate-type = ["cdylib"]
102+
required-features = ["vtab-loadable", "loadable-extension"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
extern crate duckdb;
2+
extern crate duckdb_loadable_macros;
3+
extern crate libduckdb_sys;
4+
5+
use duckdb::{
6+
core::{DataChunkHandle, Inserter, LogicalTypeHandle, LogicalTypeId},
7+
vtab::{BindInfo, Free, FunctionInfo, InitInfo, VTab},
8+
Connection, Result,
9+
};
10+
use duckdb_loadable_macros::duckdb_entrypoint_c_api;
11+
use libduckdb_sys as ffi;
12+
use std::{
13+
error::Error,
14+
ffi::{c_char, CString},
15+
};
16+
17+
#[repr(C)]
18+
struct HelloBindData {
19+
name: *mut c_char,
20+
}
21+
22+
impl Free for HelloBindData {
23+
fn free(&mut self) {
24+
unsafe {
25+
if self.name.is_null() {
26+
return;
27+
}
28+
drop(CString::from_raw(self.name));
29+
}
30+
}
31+
}
32+
33+
#[repr(C)]
34+
struct HelloInitData {
35+
done: bool,
36+
}
37+
38+
struct HelloVTab;
39+
40+
impl Free for HelloInitData {}
41+
42+
impl VTab for HelloVTab {
43+
type InitData = HelloInitData;
44+
type BindData = HelloBindData;
45+
46+
unsafe fn bind(bind: &BindInfo, data: *mut HelloBindData) -> Result<(), Box<dyn std::error::Error>> {
47+
bind.add_result_column("column0", LogicalTypeHandle::from(LogicalTypeId::Varchar));
48+
let param = bind.get_parameter(0).to_string();
49+
unsafe {
50+
(*data).name = CString::new(param).unwrap().into_raw();
51+
}
52+
Ok(())
53+
}
54+
55+
unsafe fn init(_: &InitInfo, data: *mut HelloInitData) -> Result<(), Box<dyn std::error::Error>> {
56+
unsafe {
57+
(*data).done = false;
58+
}
59+
Ok(())
60+
}
61+
62+
unsafe fn func(func: &FunctionInfo, output: &mut DataChunkHandle) -> Result<(), Box<dyn std::error::Error>> {
63+
let init_info = func.get_init_data::<HelloInitData>();
64+
let bind_info = func.get_bind_data::<HelloBindData>();
65+
66+
unsafe {
67+
if (*init_info).done {
68+
output.set_len(0);
69+
} else {
70+
(*init_info).done = true;
71+
let vector = output.flat_vector(0);
72+
let name = CString::from_raw((*bind_info).name);
73+
let result = CString::new(format!("Hello {}", name.to_str()?))?;
74+
// Can't consume the CString
75+
(*bind_info).name = CString::into_raw(name);
76+
vector.insert(0, result);
77+
output.set_len(1);
78+
}
79+
}
80+
Ok(())
81+
}
82+
83+
fn parameters() -> Option<Vec<LogicalTypeHandle>> {
84+
Some(vec![LogicalTypeHandle::from(LogicalTypeId::Varchar)])
85+
}
86+
}
87+
88+
#[duckdb_entrypoint_c_api(ext_name = "rusty_quack", min_duckdb_version = "v0.0.1")]
89+
pub fn extension_entrypoint(con: Connection) -> Result<(), Box<dyn Error>> {
90+
con.register_table_function::<HelloVTab>("hello")
91+
.expect("Failed to register hello table function");
92+
Ok(())
93+
}

0 commit comments

Comments
 (0)