Skip to content

Commit b310e9b

Browse files
authored
build loadable ext (#149)
* build loadable ext * fix warning * add loadable macros * fix comment * fix clippy error * fix clippy
1 parent 80a492c commit b310e9b

File tree

8 files changed

+196
-3
lines changed

8 files changed

+196
-3
lines changed

.github/workflows/rust.yaml

+6-1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ jobs:
9494
env:
9595
DUCKDB_LIB_DIR: ${{ github.workspace }}/libduckdb
9696
DUCKDB_INCLUDE_DIR: ${{ github.workspace }}/libduckdb
97+
- name: Build loadable extension
98+
run: cargo build --example hello-ext --features="vtab-loadable bundled"
9799

98100
Windows:
99101
name: Windows build from source
@@ -116,6 +118,8 @@ jobs:
116118
rust-version: stable
117119
targets: x86_64-pc-windows-msvc
118120
- run: cargo test --features "modern-full extensions-full"
121+
- name: Build loadable extension
122+
run: cargo build --example hello-ext --features="vtab-loadable bundled"
119123

120124
Sanitizer:
121125
name: Address Sanitizer
@@ -139,7 +143,8 @@ jobs:
139143
# as the other tests have them.
140144
RUST_BACKTRACE: "0"
141145
run: cargo -Z build-std test --features "modern-full extensions-full" --target x86_64-unknown-linux-gnu
142-
146+
- name: Build loadable extension
147+
run: cargo build --example hello-ext --features="vtab-loadable bundled"
143148
- uses: wangfenjin/publish-crates@main
144149
name: cargo publish --dry-run
145150
with:

Cargo.toml

+8-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ categories = ["database"]
1818
name = "duckdb"
1919

2020
[workspace]
21-
members = ["libduckdb-sys"]
21+
members = ["libduckdb-sys", "duckdb-loadable-macros"]
2222

2323
[features]
2424
default = []
@@ -27,6 +27,7 @@ httpfs = ["libduckdb-sys/httpfs", "bundled"]
2727
json = ["libduckdb-sys/json", "bundled"]
2828
parquet = ["libduckdb-sys/parquet", "bundled"]
2929
vtab = []
30+
vtab-loadable = ["vtab", "duckdb-loadable-macros"]
3031
vtab-excel = ["vtab", "calamine"]
3132
vtab-arrow = ["vtab", "num"]
3233
vtab-full = ["vtab-excel", "vtab-arrow"]
@@ -61,6 +62,7 @@ strum = { version = "0.24", features = ["derive"] }
6162
r2d2 = { version = "0.8.9", optional = true }
6263
calamine = { version = "0.19.1", optional = true }
6364
num = { version = "0.4", optional = true, default-features = false, features = ["std"] }
65+
duckdb-loadable-macros = { version = "0.1.0", path="./duckdb-loadable-macros", optional = true }
6466

6567
[dev-dependencies]
6668
doc-comment = "0.3"
@@ -90,3 +92,8 @@ default-target = "x86_64-unknown-linux-gnu"
9092
[package.metadata.playground]
9193
features = []
9294
all-features = false
95+
96+
[[example]]
97+
name = "hello-ext"
98+
crate-type = ["cdylib"]
99+
required-features = ["vtab-loadable", "bundled"]

duckdb-loadable-macros/Cargo.toml

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "duckdb-loadable-macros"
3+
version = "0.1.0"
4+
authors = ["wangfenjin <[email protected]>"]
5+
edition = "2021"
6+
license = "MIT"
7+
repository = "https://github.com/wangfenjin/duckdb-rs"
8+
homepage = "https://github.com/wangfenjin/duckdb-rs"
9+
keywords = ["duckdb", "ffi", "database"]
10+
readme = "README.md"
11+
categories = ["external-ffi-bindings", "database"]
12+
description = "Native bindings to the libduckdb library, C API; build loadable extensions"
13+
14+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
15+
16+
[dependencies]
17+
proc-macro2 = { version = "1.0.56" }
18+
quote = { version = "1.0.21" }
19+
syn = { version = "1.0.95", features = [ "extra-traits", "full", "fold", "parsing" ] }
20+
21+
[lib]
22+
proc-macro = true

duckdb-loadable-macros/LICENSE

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../LICENSE

duckdb-loadable-macros/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../README.md

duckdb-loadable-macros/src/lib.rs

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
use proc_macro2::Ident;
2+
3+
use syn::{parse_macro_input, spanned::Spanned, Item};
4+
5+
use proc_macro::TokenStream;
6+
use quote::quote_spanned;
7+
8+
/// Wraps an entrypoint function to expose an unsafe extern "C" function of the same name.
9+
#[proc_macro_attribute]
10+
pub fn duckdb_entrypoint(_attr: TokenStream, item: TokenStream) -> TokenStream {
11+
let ast = parse_macro_input!(item as syn::Item);
12+
match ast {
13+
Item::Fn(mut func) => {
14+
let c_entrypoint = func.sig.ident.clone();
15+
16+
let original_funcname = func.sig.ident.to_string();
17+
func.sig.ident = Ident::new(format!("_{}", original_funcname).as_str(), func.sig.ident.span());
18+
19+
let prefixed_original_function = func.sig.ident.clone();
20+
21+
quote_spanned! {func.span()=>
22+
#func
23+
24+
/// # Safety
25+
///
26+
/// Will be called by duckdb
27+
#[no_mangle]
28+
pub unsafe extern "C" fn #c_entrypoint(db: *mut c_void) {
29+
let connection = Connection::open_from_raw(db.cast()).expect("can't open db connection");
30+
#prefixed_original_function(connection).expect("init failed");
31+
}
32+
33+
/// # Safety
34+
///
35+
/// Predefined function, don't need to change unless you are sure
36+
#[no_mangle]
37+
pub unsafe extern "C" fn libhello_ext_version() -> *const c_char {
38+
ffi::duckdb_library_version()
39+
}
40+
41+
42+
}
43+
.into()
44+
}
45+
_ => panic!("Only function items are allowed on duckdb_entrypoint"),
46+
}
47+
}

examples/hello-ext/main.rs

+93
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+
vtab::{BindInfo, DataChunk, Free, FunctionInfo, InitInfo, Inserter, LogicalType, LogicalTypeId, VTab},
7+
Connection, Result,
8+
};
9+
use duckdb_loadable_macros::duckdb_entrypoint;
10+
use libduckdb_sys as ffi;
11+
use std::{
12+
error::Error,
13+
ffi::{c_char, c_void, CString},
14+
};
15+
16+
#[repr(C)]
17+
struct HelloBindData {
18+
name: *mut c_char,
19+
}
20+
21+
impl Free for HelloBindData {
22+
fn free(&mut self) {
23+
unsafe {
24+
if self.name.is_null() {
25+
return;
26+
}
27+
drop(CString::from_raw(self.name));
28+
}
29+
}
30+
}
31+
32+
#[repr(C)]
33+
struct HelloInitData {
34+
done: bool,
35+
}
36+
37+
struct HelloVTab;
38+
39+
impl Free for HelloInitData {}
40+
41+
impl VTab for HelloVTab {
42+
type InitData = HelloInitData;
43+
type BindData = HelloBindData;
44+
45+
fn bind(bind: &BindInfo, data: *mut HelloBindData) -> Result<(), Box<dyn std::error::Error>> {
46+
bind.add_result_column("column0", LogicalType::new(LogicalTypeId::Varchar));
47+
let param = bind.get_parameter(0).to_string();
48+
unsafe {
49+
(*data).name = CString::new(param).unwrap().into_raw();
50+
}
51+
Ok(())
52+
}
53+
54+
fn init(_: &InitInfo, data: *mut HelloInitData) -> Result<(), Box<dyn std::error::Error>> {
55+
unsafe {
56+
(*data).done = false;
57+
}
58+
Ok(())
59+
}
60+
61+
fn func(func: &FunctionInfo, output: &mut DataChunk) -> Result<(), Box<dyn std::error::Error>> {
62+
let init_info = func.get_init_data::<HelloInitData>();
63+
let bind_info = func.get_bind_data::<HelloBindData>();
64+
65+
unsafe {
66+
if (*init_info).done {
67+
output.set_len(0);
68+
} else {
69+
(*init_info).done = true;
70+
let vector = output.flat_vector(0);
71+
let name = CString::from_raw((*bind_info).name);
72+
let result = CString::new(format!("Hello {}", name.to_str()?))?;
73+
// Can't consume the CString
74+
(*bind_info).name = CString::into_raw(name);
75+
vector.insert(0, result);
76+
output.set_len(1);
77+
}
78+
}
79+
Ok(())
80+
}
81+
82+
fn parameters() -> Option<Vec<LogicalType>> {
83+
Some(vec![LogicalType::new(LogicalTypeId::Varchar)])
84+
}
85+
}
86+
87+
// Exposes a extern C function named "libhello_ext_init" in the compiled dynamic library,
88+
// the "entrypoint" that duckdb will use to load the extension.
89+
#[duckdb_entrypoint]
90+
pub fn libhello_ext_init(conn: Connection) -> Result<(), Box<dyn Error>> {
91+
conn.register_table_function::<HelloVTab>("hello")?;
92+
Ok(())
93+
}

src/lib.rs

+18-1
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,23 @@ impl Connection {
249249
Connection::open_in_memory_with_flags(Config::default())
250250
}
251251

252+
/// Open a new connection to an ffi database.
253+
///
254+
/// # Failure
255+
///
256+
/// Will return `Err` if the underlying DuckDB open call fails.
257+
/// # Safety
258+
///
259+
/// Need to pass in a valid db instance
260+
#[inline]
261+
pub unsafe fn open_from_raw(raw: ffi::duckdb_database) -> Result<Connection> {
262+
InnerConnection::new(raw, false).map(|db| Connection {
263+
db: RefCell::new(db),
264+
cache: StatementCache::with_capacity(STATEMENT_CACHE_DEFAULT_CAPACITY),
265+
path: None, // Can we know the path from connection?
266+
})
267+
}
268+
252269
/// Open a new connection to a DuckDB database.
253270
///
254271
/// # Failure
@@ -554,7 +571,7 @@ mod test {
554571
// this function is never called, but is still type checked; in
555572
// particular, calls with specific instantiations will require
556573
// that those types are `Send`.
557-
#[allow(dead_code, unconditional_recursion)]
574+
#[allow(dead_code, unconditional_recursion, clippy::extra_unused_type_parameters)]
558575
fn ensure_send<T: Send>() {
559576
ensure_send::<Connection>();
560577
}

0 commit comments

Comments
 (0)