From 65e07c390e2d49e48d0c12df62efd32b29909057 Mon Sep 17 00:00:00 2001
From: David Li
Date: Thu, 18 Dec 2025 06:24:44 +0900
Subject: [PATCH 1/5] feat(rust/ffi): catch panics at FFI boundary
This gives a slightly nicer experience than unconditionally
crashing the entire program. As with the Go FFI scaffolding,
once a driver panics, all further calls to the driver will fail.
(Albeit, this is not true for exported readers. We also need our
own C Data Interface export shim so we can handle things like
exporting ADBC errors through the C Data Interface boundary.
Currently, C++ and Go-based drivers can do this.)
---
.github/workflows/native-unix.yml | 6 +
.../adbc_driver_manager/tests/test_panic.py | 70 +-
rust/driver/dummy/src/lib.rs | 21 +-
.../dummy/tests/driver_exporter_dummy.rs | 2 +-
rust/ffi/src/driver_exporter.rs | 1401 ++++++++++-------
5 files changed, 935 insertions(+), 565 deletions(-)
diff --git a/.github/workflows/native-unix.yml b/.github/workflows/native-unix.yml
index ebbbbaa73d..2c4529df53 100644
--- a/.github/workflows/native-unix.yml
+++ b/.github/workflows/native-unix.yml
@@ -598,12 +598,18 @@ jobs:
else
make -C ./go/adbc/pkg libadbc_driver_panicdummy.so
fi
+ - name: Build Panic Dummy (Rust)
+ working-directory: rust
+ run: |
+ cargo build -padbc_dummy
- name: Test Python Driver Manager
run: |
if [[ $(uname) = "Darwin" ]]; then
export PANICDUMMY_LIBRARY_PATH=$(pwd)/go/adbc/pkg/libadbc_driver_panicdummy.dylib
+ export PANICDUMMY_RUST_LIBRARY_PATH=$(pwd)/rust/target/debug/libadbc_dummy.dylib
else
export PANICDUMMY_LIBRARY_PATH=$(pwd)/go/adbc/pkg/libadbc_driver_panicdummy.so
+ export PANICDUMMY_RUST_LIBRARY_PATH=$(pwd)/rust/target/debug/libadbc_dummy.so
fi
export PATH=$RUNNER_TOOL_CACHE/go/${GO_VERSION}/x64/bin:$PATH
env BUILD_ALL=0 BUILD_DRIVER_MANAGER=1 ./ci/scripts/python_test.sh "$(pwd)" "$(pwd)/build" "$HOME/local"
diff --git a/python/adbc_driver_manager/tests/test_panic.py b/python/adbc_driver_manager/tests/test_panic.py
index d0aef2b853..9963c7a8a8 100644
--- a/python/adbc_driver_manager/tests/test_panic.py
+++ b/python/adbc_driver_manager/tests/test_panic.py
@@ -15,7 +15,7 @@
# specific language governing permissions and limitations
# under the License.
-"""Tests for the panic behavior of the Go driver FFI wrapper."""
+"""Tests for the panic behavior of the Go/Rust FFI wrappers."""
import os
import subprocess
@@ -26,17 +26,25 @@
pytestmark = pytest.mark.panicdummy
-_LIB_ENV_VAR = "PANICDUMMY_LIBRARY_PATH"
+_GO_LIB_ENV_VAR = "PANICDUMMY_LIBRARY_PATH"
+_RUST_LIB_ENV_VAR = "PANICDUMMY_RUST_LIBRARY_PATH"
@pytest.fixture(scope="module")
-def libpath() -> str:
- if _LIB_ENV_VAR not in os.environ:
- pytest.skip(f"{_LIB_ENV_VAR} not specified", allow_module_level=True)
- return os.environ[_LIB_ENV_VAR]
+def go_driver() -> str:
+ if _GO_LIB_ENV_VAR not in os.environ:
+ pytest.skip(f"{_GO_LIB_ENV_VAR} not specified", allow_module_level=True)
+ return os.environ[_GO_LIB_ENV_VAR]
-def test_panic_close(libpath) -> None:
+@pytest.fixture(scope="module")
+def rust_driver() -> str:
+ if _RUST_LIB_ENV_VAR not in os.environ:
+ pytest.skip(f"{_RUST_LIB_ENV_VAR} not specified", allow_module_level=True)
+ return os.environ[_RUST_LIB_ENV_VAR]
+
+
+def test_panic_close_go(go_driver) -> None:
env = os.environ.copy()
env["PANICDUMMY_FUNC"] = "StatementClose"
env["PANICDUMMY_MESSAGE"] = "Boo!"
@@ -44,7 +52,7 @@ def test_panic_close(libpath) -> None:
[
sys.executable,
Path(__file__).parent / "panictest.py",
- libpath,
+ go_driver,
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
@@ -57,7 +65,7 @@ def test_panic_close(libpath) -> None:
assert "Go panicked, driver is in unknown state" in output.stderr
-def test_panic_execute(libpath) -> None:
+def test_panic_execute_go(go_driver) -> None:
env = os.environ.copy()
env["PANICDUMMY_FUNC"] = "StatementExecuteQuery"
env["PANICDUMMY_MESSAGE"] = "Boo!"
@@ -65,7 +73,7 @@ def test_panic_execute(libpath) -> None:
[
sys.executable,
Path(__file__).parent / "panictest.py",
- libpath,
+ go_driver,
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
@@ -76,3 +84,45 @@ def test_panic_execute(libpath) -> None:
assert "Go panic in PanicDummy driver" in output.stderr
assert "Boo!" in output.stderr
assert "Go panicked, driver is in unknown state" in output.stderr
+
+
+def test_panic_close_rust(rust_driver) -> None:
+ env = os.environ.copy()
+ env["PANICDUMMY_FUNC"] = "StatementClose"
+ env["PANICDUMMY_MESSAGE"] = "Boo!"
+ output = subprocess.run(
+ [
+ sys.executable,
+ Path(__file__).parent / "panictest.py",
+ rust_driver,
+ ],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=env,
+ encoding="utf-8",
+ )
+ assert output.returncode != 0
+ assert "Uncaught panic in driver" in output.stderr
+ assert "Boo!" in output.stderr
+ assert "Driver panicked and is in unknown state" in output.stderr
+
+
+def test_panic_execute_rust(rust_driver) -> None:
+ env = os.environ.copy()
+ env["PANICDUMMY_FUNC"] = "StatementExecuteQuery"
+ env["PANICDUMMY_MESSAGE"] = "Boo!"
+ output = subprocess.run(
+ [
+ sys.executable,
+ Path(__file__).parent / "panictest.py",
+ rust_driver,
+ ],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=env,
+ encoding="utf-8",
+ )
+ assert output.returncode != 0
+ assert "Uncaught panic in driver" in output.stderr
+ assert "Boo!" in output.stderr
+ assert "Driver panicked and is in unknown state" in output.stderr
diff --git a/rust/driver/dummy/src/lib.rs b/rust/driver/dummy/src/lib.rs
index 4ab708afdf..451b895941 100644
--- a/rust/driver/dummy/src/lib.rs
+++ b/rust/driver/dummy/src/lib.rs
@@ -175,6 +175,18 @@ where
}
}
+fn maybe_panic(fnname: impl AsRef) {
+ if let Some(func) = std::env::var_os("PANICDUMMY_FUNC").map(|x| x.to_string_lossy().to_string())
+ {
+ if fnname.as_ref() == func {
+ let message = std::env::var_os("PANICDUMMY_MESSAGE")
+ .map(|x| x.to_string_lossy().to_string())
+ .unwrap_or_else(|| format!("We panicked in {}!", fnname.as_ref()));
+ panic!("{}", message);
+ }
+ }
+}
+
/// A dummy driver used for testing purposes.
#[derive(Default)]
pub struct DummyDriver {}
@@ -837,6 +849,7 @@ impl Statement for DummyStatement {
}
fn execute(&mut self) -> Result {
+ maybe_panic("StatementExecuteQuery");
let batch = get_table_data();
let reader = SingleBatchReader::new(batch);
Ok(reader)
@@ -875,4 +888,10 @@ impl Statement for DummyStatement {
}
}
-adbc_ffi::export_driver!(DummyDriverInit, DummyDriver);
+impl Drop for DummyStatement {
+ fn drop(&mut self) {
+ maybe_panic("StatementClose");
+ }
+}
+
+adbc_ffi::export_driver!(AdbcDummyInit, DummyDriver);
diff --git a/rust/driver/dummy/tests/driver_exporter_dummy.rs b/rust/driver/dummy/tests/driver_exporter_dummy.rs
index a5869c0555..6d1445a45b 100644
--- a/rust/driver/dummy/tests/driver_exporter_dummy.rs
+++ b/rust/driver/dummy/tests/driver_exporter_dummy.rs
@@ -67,7 +67,7 @@ fn get_exported() -> (
) {
let mut driver = ManagedDriver::load_dynamic_from_name(
"adbc_dummy",
- Some(b"DummyDriverInit"),
+ Some(b"AdbcDummyInit"),
AdbcVersion::V110,
)
.unwrap();
diff --git a/rust/ffi/src/driver_exporter.rs b/rust/ffi/src/driver_exporter.rs
index c142121bf1..048c171105 100644
--- a/rust/ffi/src/driver_exporter.rs
+++ b/rust/ffi/src/driver_exporter.rs
@@ -180,6 +180,7 @@ impl FFIDriver for DriverType {
#[macro_export]
macro_rules! export_driver {
($func_name:ident, $driver_type:ty) => {
+ #[allow(non_snake_case)]
#[no_mangle]
pub unsafe extern "C" fn $func_name(
version: std::os::raw::c_int,
@@ -207,6 +208,16 @@ macro_rules! export_driver {
}
adbc_core::constants::ADBC_STATUS_OK
}
+
+ #[allow(non_snake_case)]
+ #[no_mangle]
+ pub unsafe extern "C" fn AdbcDriverInit(
+ version: std::os::raw::c_int,
+ driver: *mut std::os::raw::c_void,
+ error: *mut adbc_ffi::FFI_AdbcError,
+ ) -> adbc_core::error::AdbcStatusCode {
+ unsafe { $func_name(version, driver, error) }
+ }
};
}
@@ -229,7 +240,7 @@ macro_rules! check_err {
if !$err_out.is_null() {
let mut ffi_error =
$crate::FFI_AdbcError::try_from(error).unwrap_or_else(Into::into);
- ffi_error.private_driver = (*$err_out).private_driver;
+ ffi_error.private_driver = unsafe { (*$err_out).private_driver };
unsafe { std::ptr::write_unaligned($err_out, ffi_error) };
}
return status;
@@ -258,6 +269,32 @@ macro_rules! check_not_null {
};
}
+/// Check that the given raw pointer is not null, then return it as a mutable reference.
+///
+/// If null, an error is returned from the enclosing function.
+#[doc(hidden)]
+#[macro_export]
+macro_rules! pointer_as_mut {
+ ($ptr:ident, $err_out:expr) => {
+ match unsafe { $ptr.as_mut() } {
+ Some(p) => p,
+ None => {
+ if !$err_out.is_null() {
+ let error = adbc_core::error::Error::with_message_and_status(
+ format!("Passed null pointer for argument {:?}", stringify!($ptr)),
+ adbc_core::error::Status::InvalidArguments,
+ );
+ let mut ffi_error =
+ $crate::FFI_AdbcError::try_from(error).unwrap_or_else(Into::into);
+ ffi_error.private_driver = unsafe { (*$err_out).private_driver };
+ unsafe { std::ptr::write_unaligned($err_out, ffi_error) };
+ }
+ return adbc_core::error::Status::InvalidArguments.into();
+ }
+ }
+ };
+}
+
unsafe extern "C" fn release_ffi_driver(
driver: *mut FFI_AdbcDriver,
error: *mut FFI_AdbcError,
@@ -456,14 +493,57 @@ where
}
}
+static POISON: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
+
+fn catch_panic AdbcStatusCode + std::panic::UnwindSafe>(
+ error: *mut FFI_AdbcError,
+ f: F,
+) -> AdbcStatusCode {
+ check_err!(check_poison(), error);
+
+ match std::panic::catch_unwind(f) {
+ Ok(status) => status,
+ Err(panic) => {
+ POISON.store(true, std::sync::atomic::Ordering::Release);
+
+ if !error.is_null() {
+ let message = if let Some(s) = panic.downcast_ref::<&str>() {
+ s.to_string()
+ } else if let Some(s) = panic.downcast_ref::() {
+ s.clone()
+ } else {
+ "Unknown panic".to_string()
+ };
+ let err = Error::with_message_and_status(
+ format!("Uncaught panic in driver: {message}"),
+ Status::Internal,
+ );
+ let mut ffi_error = FFI_AdbcError::try_from(err).unwrap_or_else(Into::into);
+ ffi_error.private_driver = unsafe { (*error).private_driver };
+ unsafe { std::ptr::write_unaligned(error, ffi_error) };
+ }
+ Status::Internal.into()
+ }
+ }
+}
+
+fn check_poison() -> Result<()> {
+ if POISON.load(std::sync::atomic::Ordering::Acquire) {
+ Err(Error::with_message_and_status(
+ "Driver panicked and is in unknown state",
+ Status::Internal,
+ ))
+ } else {
+ Ok(())
+ }
+}
+
// Database
-// SAFETY: Will panic if `database` is null.
unsafe fn database_private_data<'a, DriverType: Driver>(
- database: *mut FFI_AdbcDatabase,
+ database: &mut FFI_AdbcDatabase,
) -> Result<&'a mut ExportedDatabase> {
- assert!(!database.is_null());
- let exported = (*database).private_data as *mut ExportedDatabase;
+ let exported = database.private_data as *mut ExportedDatabase;
let exported = exported.as_mut().ok_or(Error::with_message_and_status(
"Uninitialized database",
Status::InvalidState,
@@ -471,14 +551,13 @@ unsafe fn database_private_data<'a, DriverType: Driver>(
exported
}
-// SAFETY: Will panic if `database` or `key` is null.
+// SAFETY: Will panic if `key` is null.
unsafe fn database_set_option_impl>(
- database: *mut FFI_AdbcDatabase,
+ database: &mut FFI_AdbcDatabase,
key: *const c_char,
value: Value,
error: *mut FFI_AdbcError,
) -> AdbcStatusCode {
- assert!(!database.is_null());
assert!(!key.is_null());
let exported = check_err!(database_private_data::(database), error);
@@ -496,200 +575,234 @@ unsafe fn database_set_option_impl>
ADBC_STATUS_OK
}
-unsafe extern "C" fn database_new(
+extern "C" fn database_new(
database: *mut FFI_AdbcDatabase,
error: *mut FFI_AdbcError,
) -> AdbcStatusCode {
- check_not_null!(database, error);
+ catch_panic(error, || {
+ let database = pointer_as_mut!(database, error);
+ let exported = Box::new(ExportedDatabase::::Options(HashMap::new()));
+ database.private_data = Box::into_raw(exported) as *mut c_void;
- let database = database.as_mut().unwrap();
- let exported = Box::new(ExportedDatabase::::Options(HashMap::new()));
- database.private_data = Box::into_raw(exported) as *mut c_void;
-
- ADBC_STATUS_OK
+ ADBC_STATUS_OK
+ })
}
-unsafe extern "C" fn database_init(
+extern "C" fn database_init(
database: *mut FFI_AdbcDatabase,
error: *mut FFI_AdbcError,
) -> AdbcStatusCode {
- check_not_null!(database, error);
+ catch_panic(error, || {
+ let database = pointer_as_mut!(database, error);
- let exported = check_err!(database_private_data::(database), error);
-
- if let ExportedDatabase::Options(options) = exported {
- let mut driver = DriverType::default();
- let database = check_err!(driver.new_database_with_opts(options.clone()), error);
- *exported = ExportedDatabase::Database(database);
- } else {
- check_err!(
- Err(Error::with_message_and_status(
- "Database already initialized",
- Status::InvalidState
- )),
+ let exported = check_err!(
+ unsafe { database_private_data::(database) },
error
);
- }
- ADBC_STATUS_OK
+ if let ExportedDatabase::Options(options) = exported {
+ let mut driver = DriverType::default();
+ let database = check_err!(driver.new_database_with_opts(options.clone()), error);
+ *exported = ExportedDatabase::Database(database);
+ } else {
+ check_err!(
+ Err(Error::with_message_and_status(
+ "Database already initialized",
+ Status::InvalidState
+ )),
+ error
+ );
+ }
+
+ ADBC_STATUS_OK
+ })
}
-unsafe extern "C" fn database_release(
+extern "C" fn database_release(
database: *mut FFI_AdbcDatabase,
error: *mut FFI_AdbcError,
) -> AdbcStatusCode {
- check_not_null!(database, error);
-
- let database = database.as_mut().unwrap();
- if database.private_data.is_null() {
- check_err!(
- Err(Error::with_message_and_status(
- "Database already released",
- Status::InvalidState
- )),
- error
- );
- }
- let exported = Box::from_raw(database.private_data as *mut ExportedDatabase);
- drop(exported);
- database.private_data = std::ptr::null_mut();
+ catch_panic(error, || {
+ let database = pointer_as_mut!(database, error);
+ if database.private_data.is_null() {
+ check_err!(
+ Err(Error::with_message_and_status(
+ "Database already released",
+ Status::InvalidState
+ )),
+ error
+ );
+ }
+ let exported =
+ unsafe { Box::from_raw(database.private_data as *mut ExportedDatabase) };
+ drop(exported);
+ database.private_data = std::ptr::null_mut();
- ADBC_STATUS_OK
+ ADBC_STATUS_OK
+ })
}
-unsafe extern "C" fn database_set_option(
+extern "C" fn database_set_option(
database: *mut FFI_AdbcDatabase,
key: *const c_char,
value: *const c_char,
error: *mut FFI_AdbcError,
) -> AdbcStatusCode {
- check_not_null!(database, error);
- check_not_null!(key, error);
- check_not_null!(value, error);
+ catch_panic(error, || {
+ let database = pointer_as_mut!(database, error);
+ check_not_null!(key, error);
+ check_not_null!(value, error);
- let value = check_err!(CStr::from_ptr(value).to_str(), error);
- database_set_option_impl::(database, key, value, error)
+ let value = check_err!(unsafe { CStr::from_ptr(value).to_str() }, error);
+ unsafe { database_set_option_impl::(database, key, value, error) }
+ })
}
-unsafe extern "C" fn database_set_option_int(
+extern "C" fn database_set_option_int(
database: *mut FFI_AdbcDatabase,
key: *const c_char,
value: i64,
error: *mut FFI_AdbcError,
) -> AdbcStatusCode {
- check_not_null!(database, error);
- check_not_null!(key, error);
+ catch_panic(error, || {
+ let database = pointer_as_mut!(database, error);
+ check_not_null!(key, error);
- database_set_option_impl::(database, key, value, error)
+ unsafe { database_set_option_impl::(database, key, value, error) }
+ })
}
-unsafe extern "C" fn database_set_option_double(
+extern "C" fn database_set_option_double(
database: *mut FFI_AdbcDatabase,
key: *const c_char,
value: f64,
error: *mut FFI_AdbcError,
) -> AdbcStatusCode {
- check_not_null!(database, error);
- check_not_null!(key, error);
+ catch_panic(error, || {
+ let database = pointer_as_mut!(database, error);
+ check_not_null!(key, error);
- database_set_option_impl::(database, key, value, error)
+ unsafe { database_set_option_impl::(database, key, value, error) }
+ })
}
-unsafe extern "C" fn database_set_option_bytes(
+extern "C" fn database_set_option_bytes(
database: *mut FFI_AdbcDatabase,
key: *const c_char,
value: *const u8,
length: usize,
error: *mut FFI_AdbcError,
) -> AdbcStatusCode {
- check_not_null!(database, error);
- check_not_null!(key, error);
- check_not_null!(value, error);
+ catch_panic(error, || {
+ let database = pointer_as_mut!(database, error);
+ check_not_null!(key, error);
+ check_not_null!(value, error);
- let value = std::slice::from_raw_parts(value, length);
- database_set_option_impl::(database, key, value, error)
+ let value = unsafe { std::slice::from_raw_parts(value, length) };
+ unsafe { database_set_option_impl::(database, key, value, error) }
+ })
}
-unsafe extern "C" fn database_get_option(
+extern "C" fn database_get_option(
database: *mut FFI_AdbcDatabase,
key: *const c_char,
value: *mut c_char,
length: *mut usize,
error: *mut FFI_AdbcError,
) -> AdbcStatusCode {
- check_not_null!(database, error);
- check_not_null!(key, error);
- check_not_null!(value, error);
- check_not_null!(length, error);
-
- let exported = check_err!(database_private_data::(database), error);
- let (options, database) = exported.tuple();
+ catch_panic(error, || {
+ let database = pointer_as_mut!(database, error);
+ check_not_null!(key, error);
+ check_not_null!(value, error);
+ check_not_null!(length, error);
+
+ let exported = check_err!(
+ unsafe { database_private_data::(database) },
+ error
+ );
+ let (options, database) = exported.tuple();
- let optvalue = get_option(database, options, key);
- let optvalue = check_err!(optvalue, error);
- check_err!(copy_string(&optvalue, value, length), error);
+ let optvalue = unsafe { get_option(database, options, key) };
+ let optvalue = check_err!(optvalue, error);
+ check_err!(unsafe { copy_string(&optvalue, value, length) }, error);
- ADBC_STATUS_OK
+ ADBC_STATUS_OK
+ })
}
-unsafe extern "C" fn database_get_option_int(
+extern "C" fn database_get_option_int(
database: *mut FFI_AdbcDatabase,
key: *const c_char,
value: *mut i64,
error: *mut FFI_AdbcError,
) -> AdbcStatusCode {
- check_not_null!(database, error);
- check_not_null!(key, error);
- check_not_null!(value, error);
+ catch_panic(error, || {
+ let database = pointer_as_mut!(database, error);
+ check_not_null!(key, error);
+ check_not_null!(value, error);
- let exported = check_err!(database_private_data::(database), error);
- let (options, database) = exported.tuple();
+ let exported = check_err!(
+ unsafe { database_private_data::(database) },
+ error
+ );
+ let (options, database) = exported.tuple();
- let optvalue = check_err!(get_option_int(database, options, key), error);
- std::ptr::write_unaligned(value, optvalue);
+ let optvalue = check_err!(unsafe { get_option_int(database, options, key) }, error);
+ unsafe { std::ptr::write_unaligned(value, optvalue) };
- ADBC_STATUS_OK
+ ADBC_STATUS_OK
+ })
}
-unsafe extern "C" fn database_get_option_double(
+extern "C" fn database_get_option_double(
database: *mut FFI_AdbcDatabase,
key: *const c_char,
value: *mut f64,
error: *mut FFI_AdbcError,
) -> AdbcStatusCode {
- check_not_null!(database, error);
- check_not_null!(key, error);
- check_not_null!(value, error);
+ catch_panic(error, || {
+ let database = pointer_as_mut!(database, error);
+ check_not_null!(key, error);
+ check_not_null!(value, error);
- let exported = check_err!(database_private_data::(database), error);
- let (options, database) = exported.tuple();
+ let exported = check_err!(
+ unsafe { database_private_data::(database) },
+ error
+ );
+ let (options, database) = exported.tuple();
- let optvalue = check_err!(get_option_double(database, options, key), error);
- std::ptr::write_unaligned(value, optvalue);
+ let optvalue = check_err!(unsafe { get_option_double(database, options, key) }, error);
+ unsafe { std::ptr::write_unaligned(value, optvalue) };
- ADBC_STATUS_OK
+ ADBC_STATUS_OK
+ })
}
-unsafe extern "C" fn database_get_option_bytes(
+extern "C" fn database_get_option_bytes(
database: *mut FFI_AdbcDatabase,
key: *const c_char,
value: *mut u8,
length: *mut usize,
error: *mut FFI_AdbcError,
) -> AdbcStatusCode {
- check_not_null!(database, error);
- check_not_null!(key, error);
- check_not_null!(value, error);
- check_not_null!(length, error);
-
- let exported = check_err!(database_private_data::(database), error);
- let (options, database) = exported.tuple();
+ catch_panic(error, || {
+ let database = pointer_as_mut!(database, error);
+ check_not_null!(key, error);
+ check_not_null!(value, error);
+ check_not_null!(length, error);
+
+ let exported = check_err!(
+ unsafe { database_private_data::(database) },
+ error
+ );
+ let (options, database) = exported.tuple();
- let optvalue = get_option_bytes(database, options, key);
- let optvalue = check_err!(optvalue, error);
- copy_bytes(&optvalue, value, length);
+ let optvalue = unsafe { get_option_bytes(database, options, key) };
+ let optvalue = check_err!(optvalue, error);
+ unsafe { copy_bytes(&optvalue, value, length) };
- ADBC_STATUS_OK
+ ADBC_STATUS_OK
+ })
}
unsafe fn maybe_str<'a>(str: *const c_char) -> Result