This is an automated email from the ASF dual-hosted git repository.
lzljs3620320 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git
The following commit(s) were added to refs/heads/main by this push:
new 4db64be feat: introduce c binding to expose ffi for go binding (#123)
4db64be is described below
commit 4db64be1780b1c1f682313bdf93dab600078993f
Author: yuxia Luo <[email protected]>
AuthorDate: Sat Mar 14 09:39:53 2026 +0800
feat: introduce c binding to expose ffi for go binding (#123)
---
Cargo.toml | 7 +-
Cargo.toml => bindings/c/Cargo.toml | 29 +--
bindings/c/src/catalog.rs | 114 +++++++++++
bindings/c/src/error.rs | 120 ++++++++++++
bindings/c/src/identifier.rs | 74 +++++++
bindings/c/src/lib.rs | 36 ++++
bindings/c/src/result.rs | 73 +++++++
bindings/c/src/table.rs | 376 ++++++++++++++++++++++++++++++++++++
bindings/c/src/types.rs | 102 ++++++++++
9 files changed, 915 insertions(+), 16 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index 0ae4fa0..7384d66 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,7 +17,7 @@
[workspace]
resolver = "2"
-members = ["crates/paimon", "crates/integration_tests"]
+members = ["crates/paimon", "crates/integration_tests", "bindings/c"]
[workspace.package]
version = "0.0.0"
@@ -28,5 +28,6 @@ license = "Apache-2.0"
rust-version = "1.86.0"
[workspace.dependencies]
-arrow-array = "57.0"
-parquet = "57.0"
\ No newline at end of file
+arrow-array = { version = "57.0", features = ["ffi"] }
+parquet = "57.0"
+tokio = "1.39.2"
\ No newline at end of file
diff --git a/Cargo.toml b/bindings/c/Cargo.toml
similarity index 65%
copy from Cargo.toml
copy to bindings/c/Cargo.toml
index 0ae4fa0..a6e27cf 100644
--- a/Cargo.toml
+++ b/bindings/c/Cargo.toml
@@ -15,18 +15,21 @@
# specific language governing permissions and limitations
# under the License.
-[workspace]
-resolver = "2"
-members = ["crates/paimon", "crates/integration_tests"]
+[package]
+name = "paimon-c"
+description = "C bindings for Apache Paimon Rust"
-[workspace.package]
-version = "0.0.0"
-edition = "2021"
-homepage = "https://paimon.apache.org/"
-repository = "https://github.com/apache/paimon-rust"
-license = "Apache-2.0"
-rust-version = "1.86.0"
+repository.workspace = true
+edition.workspace = true
+license.workspace = true
+version.workspace = true
-[workspace.dependencies]
-arrow-array = "57.0"
-parquet = "57.0"
\ No newline at end of file
+[lib]
+crate-type = ["cdylib", "staticlib"]
+doc = false
+
+[dependencies]
+paimon = { path = "../../crates/paimon" }
+tokio = { workspace = true, features = ["rt-multi-thread"] }
+futures = "0.3"
+arrow-array = { workspace = true }
diff --git a/bindings/c/src/catalog.rs b/bindings/c/src/catalog.rs
new file mode 100644
index 0000000..0a9f825
--- /dev/null
+++ b/bindings/c/src/catalog.rs
@@ -0,0 +1,114 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::ffi::{c_char, c_void};
+
+use paimon::catalog::Identifier;
+use paimon::{Catalog, FileSystemCatalog};
+
+use crate::error::{check_non_null, paimon_error, validate_cstr};
+use crate::result::{paimon_result_catalog_new, paimon_result_get_table};
+use crate::runtime;
+use crate::types::{paimon_catalog, paimon_table};
+
+/// Create a new FileSystemCatalog.
+///
+/// # Safety
+/// `warehouse` must be a valid null-terminated C string, or null (returns
error).
+#[no_mangle]
+pub unsafe extern "C" fn paimon_catalog_new(warehouse: *const c_char) ->
paimon_result_catalog_new {
+ let warehouse_str = match validate_cstr(warehouse, "warehouse") {
+ Ok(s) => s,
+ Err(e) => {
+ return paimon_result_catalog_new {
+ catalog: std::ptr::null_mut(),
+ error: e,
+ }
+ }
+ };
+ match FileSystemCatalog::new(warehouse_str) {
+ Ok(catalog) => {
+ let wrapper = Box::new(paimon_catalog {
+ inner: Box::into_raw(Box::new(catalog)) as *mut c_void,
+ });
+ paimon_result_catalog_new {
+ catalog: Box::into_raw(wrapper),
+ error: std::ptr::null_mut(),
+ }
+ }
+ Err(e) => paimon_result_catalog_new {
+ catalog: std::ptr::null_mut(),
+ error: paimon_error::from_paimon(e),
+ },
+ }
+}
+
+/// Free a paimon_catalog.
+///
+/// # Safety
+/// Only call with a catalog returned from `paimon_catalog_new`.
+#[no_mangle]
+pub unsafe extern "C" fn paimon_catalog_free(catalog: *mut paimon_catalog) {
+ if !catalog.is_null() {
+ let c = Box::from_raw(catalog);
+ if !c.inner.is_null() {
+ drop(Box::from_raw(c.inner as *mut FileSystemCatalog));
+ }
+ }
+}
+
+/// Get a table from the catalog.
+///
+/// # Safety
+/// `catalog` and `identifier` must be valid pointers from previous paimon C
calls, or null (returns error).
+#[no_mangle]
+pub unsafe extern "C" fn paimon_catalog_get_table(
+ catalog: *const paimon_catalog,
+ identifier: *const crate::types::paimon_identifier,
+) -> paimon_result_get_table {
+ if let Err(e) = check_non_null(catalog, "catalog") {
+ return paimon_result_get_table {
+ table: std::ptr::null_mut(),
+ error: e,
+ };
+ }
+ if let Err(e) = check_non_null(identifier, "identifier") {
+ return paimon_result_get_table {
+ table: std::ptr::null_mut(),
+ error: e,
+ };
+ }
+
+ let catalog_ref = &*((*catalog).inner as *const FileSystemCatalog);
+ let identifier_ref = &*((*identifier).inner as *const Identifier);
+
+ match runtime().block_on(catalog_ref.get_table(identifier_ref)) {
+ Ok(table) => {
+ let wrapper = Box::new(paimon_table {
+ inner: Box::into_raw(Box::new(table)) as *mut c_void,
+ });
+ paimon_result_get_table {
+ table: Box::into_raw(wrapper),
+ error: std::ptr::null_mut(),
+ }
+ }
+ Err(e) => paimon_result_get_table {
+ table: std::ptr::null_mut(),
+ error: paimon_error::from_paimon(e),
+ },
+ }
+}
diff --git a/bindings/c/src/error.rs b/bindings/c/src/error.rs
new file mode 100644
index 0000000..7b0fbb4
--- /dev/null
+++ b/bindings/c/src/error.rs
@@ -0,0 +1,120 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::ffi::{c_char, CStr};
+
+use crate::types::paimon_bytes;
+
+/// Error codes for paimon C API.
+#[repr(i32)]
+#[allow(dead_code)]
+pub enum PaimonErrorCode {
+ Unexpected = 0,
+ Unsupported = 1,
+ NotFound = 2,
+ AlreadyExists = 3,
+ InvalidInput = 4,
+ IoError = 5,
+}
+
+/// C-compatible error type.
+#[repr(C)]
+pub struct paimon_error {
+ pub code: i32,
+ pub message: paimon_bytes,
+}
+
+impl paimon_error {
+ pub fn new(code: PaimonErrorCode, msg: String) -> *mut Self {
+ Box::into_raw(Box::new(Self {
+ code: code as i32,
+ message: paimon_bytes::new(msg.into_bytes()),
+ }))
+ }
+
+ pub fn from_paimon(e: paimon::Error) -> *mut Self {
+ let code = match &e {
+ paimon::Error::Unsupported { .. } | paimon::Error::IoUnsupported {
.. } => {
+ PaimonErrorCode::Unsupported
+ }
+ paimon::Error::TableNotExist { .. }
+ | paimon::Error::DatabaseNotExist { .. }
+ | paimon::Error::ColumnNotExist { .. } =>
PaimonErrorCode::NotFound,
+ paimon::Error::TableAlreadyExist { .. }
+ | paimon::Error::DatabaseAlreadyExist { .. }
+ | paimon::Error::ColumnAlreadyExist { .. } =>
PaimonErrorCode::AlreadyExists,
+ paimon::Error::ConfigInvalid { .. }
+ | paimon::Error::DataTypeInvalid { .. }
+ | paimon::Error::IdentifierInvalid { .. } =>
PaimonErrorCode::InvalidInput,
+ paimon::Error::IoUnexpected { .. } => PaimonErrorCode::IoError,
+ _ => PaimonErrorCode::Unexpected,
+ };
+ Self::new(code, e.to_string())
+ }
+}
+
+/// Free a paimon_error.
+///
+/// # Safety
+/// Only call with errors returned from paimon C functions.
+#[no_mangle]
+pub unsafe extern "C" fn paimon_error_free(err: *mut paimon_error) {
+ if !err.is_null() {
+ let e = Box::from_raw(err);
+ paimon_bytes_free(e.message);
+ }
+}
+
+// Re-use the bytes free from types - but we need it here for the error drop
+use crate::types::paimon_bytes_free;
+
+/// Validate a C string pointer: checks for null and valid UTF-8.
+///
+/// Returns `Ok(String)` on success, or `Err(*mut paimon_error)` if the
+/// pointer is null or the string contains invalid UTF-8.
+///
+/// # Safety
+/// If `ptr` is non-null, it must point to a valid null-terminated C string.
+pub unsafe fn validate_cstr(ptr: *const c_char, name: &str) -> Result<String,
*mut paimon_error> {
+ if ptr.is_null() {
+ return Err(paimon_error::new(
+ PaimonErrorCode::InvalidInput,
+ format!("null pointer passed for `{name}`"),
+ ));
+ }
+ CStr::from_ptr(ptr)
+ .to_str()
+ .map(|s| s.to_owned())
+ .map_err(|e| {
+ paimon_error::new(
+ PaimonErrorCode::InvalidInput,
+ format!("`{name}` is not valid UTF-8: {e}"),
+ )
+ })
+}
+
+/// Check that a pointer is non-null, returning an error if it is.
+pub fn check_non_null<T>(ptr: *const T, name: &str) -> Result<(), *mut
paimon_error> {
+ if ptr.is_null() {
+ Err(paimon_error::new(
+ PaimonErrorCode::InvalidInput,
+ format!("null pointer passed for `{name}`"),
+ ))
+ } else {
+ Ok(())
+ }
+}
diff --git a/bindings/c/src/identifier.rs b/bindings/c/src/identifier.rs
new file mode 100644
index 0000000..1ccb36d
--- /dev/null
+++ b/bindings/c/src/identifier.rs
@@ -0,0 +1,74 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::ffi::{c_char, c_void};
+
+use paimon::catalog::Identifier;
+
+use crate::error::validate_cstr;
+use crate::result::paimon_result_identifier_new;
+use crate::types::paimon_identifier;
+
+/// Create a new Identifier.
+///
+/// # Safety
+/// `database` and `object` must be valid null-terminated C strings, or null
(returns error).
+#[no_mangle]
+pub unsafe extern "C" fn paimon_identifier_new(
+ database: *const c_char,
+ object: *const c_char,
+) -> paimon_result_identifier_new {
+ let db = match validate_cstr(database, "database") {
+ Ok(s) => s,
+ Err(e) => {
+ return paimon_result_identifier_new {
+ identifier: std::ptr::null_mut(),
+ error: e,
+ }
+ }
+ };
+ let obj = match validate_cstr(object, "object") {
+ Ok(s) => s,
+ Err(e) => {
+ return paimon_result_identifier_new {
+ identifier: std::ptr::null_mut(),
+ error: e,
+ }
+ }
+ };
+ let wrapper = Box::new(paimon_identifier {
+ inner: Box::into_raw(Box::new(Identifier::new(db, obj))) as *mut
c_void,
+ });
+ paimon_result_identifier_new {
+ identifier: Box::into_raw(wrapper),
+ error: std::ptr::null_mut(),
+ }
+}
+
+/// Free a paimon_identifier.
+///
+/// # Safety
+/// Only call with an identifier returned from `paimon_identifier_new`.
+#[no_mangle]
+pub unsafe extern "C" fn paimon_identifier_free(id: *mut paimon_identifier) {
+ if !id.is_null() {
+ let i = Box::from_raw(id);
+ if !i.inner.is_null() {
+ drop(Box::from_raw(i.inner as *mut Identifier));
+ }
+ }
+}
diff --git a/bindings/c/src/lib.rs b/bindings/c/src/lib.rs
new file mode 100644
index 0000000..19942c6
--- /dev/null
+++ b/bindings/c/src/lib.rs
@@ -0,0 +1,36 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// This crate is the C binding for the Paimon project.
+// So it's type node can't meet camel case.
+#![allow(non_camel_case_types)]
+
+mod catalog;
+mod error;
+mod identifier;
+mod result;
+mod table;
+mod types;
+
+use std::sync::OnceLock;
+use tokio::runtime::Runtime;
+
+static RUNTIME: OnceLock<Runtime> = OnceLock::new();
+
+fn runtime() -> &'static Runtime {
+ RUNTIME.get_or_init(|| Runtime::new().expect("Failed to create tokio
runtime"))
+}
diff --git a/bindings/c/src/result.rs b/bindings/c/src/result.rs
new file mode 100644
index 0000000..a4fd62b
--- /dev/null
+++ b/bindings/c/src/result.rs
@@ -0,0 +1,73 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use crate::error::paimon_error;
+use crate::types::*;
+
+#[repr(C)]
+pub struct paimon_result_catalog_new {
+ pub catalog: *mut paimon_catalog,
+ pub error: *mut paimon_error,
+}
+
+#[repr(C)]
+pub struct paimon_result_identifier_new {
+ pub identifier: *mut paimon_identifier,
+ pub error: *mut paimon_error,
+}
+
+#[repr(C)]
+pub struct paimon_result_get_table {
+ pub table: *mut paimon_table,
+ pub error: *mut paimon_error,
+}
+
+#[repr(C)]
+pub struct paimon_result_new_read {
+ pub read: *mut paimon_table_read,
+ pub error: *mut paimon_error,
+}
+
+#[repr(C)]
+pub struct paimon_result_read_builder {
+ pub read_builder: *mut paimon_read_builder,
+ pub error: *mut paimon_error,
+}
+
+#[repr(C)]
+pub struct paimon_result_table_scan {
+ pub scan: *mut paimon_table_scan,
+ pub error: *mut paimon_error,
+}
+
+#[repr(C)]
+pub struct paimon_result_plan {
+ pub plan: *mut paimon_plan,
+ pub error: *mut paimon_error,
+}
+
+#[repr(C)]
+pub struct paimon_result_record_batch_reader {
+ pub reader: *mut paimon_record_batch_reader,
+ pub error: *mut paimon_error,
+}
+
+#[repr(C)]
+pub struct paimon_result_next_batch {
+ pub batch: paimon_arrow_batch,
+ pub error: *mut paimon_error,
+}
diff --git a/bindings/c/src/table.rs b/bindings/c/src/table.rs
new file mode 100644
index 0000000..7f645c9
--- /dev/null
+++ b/bindings/c/src/table.rs
@@ -0,0 +1,376 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::ffi::c_void;
+
+use arrow_array::ffi::{FFI_ArrowArray, FFI_ArrowSchema};
+use arrow_array::{Array, StructArray};
+use futures::StreamExt;
+use paimon::table::{ArrowRecordBatchStream, Table};
+use paimon::Plan;
+
+use crate::error::{check_non_null, paimon_error};
+use crate::result::{
+ paimon_result_new_read, paimon_result_next_batch, paimon_result_plan,
+ paimon_result_read_builder, paimon_result_record_batch_reader,
paimon_result_table_scan,
+};
+use crate::runtime;
+use crate::types::*;
+
+// Helper to box a Table clone into a wrapper struct and return a raw pointer.
+unsafe fn box_table_wrapper<T>(table: &Table, make: impl FnOnce(*mut c_void)
-> T) -> *mut T {
+ let inner = Box::into_raw(Box::new(table.clone())) as *mut c_void;
+ Box::into_raw(Box::new(make(inner)))
+}
+
+// Helper to free a wrapper struct that contains a Table clone.
+unsafe fn free_table_wrapper<T>(ptr: *mut T, get_inner: impl FnOnce(&T) ->
*mut c_void) {
+ if !ptr.is_null() {
+ let wrapper = Box::from_raw(ptr);
+ let inner = get_inner(&wrapper);
+ if !inner.is_null() {
+ drop(Box::from_raw(inner as *mut Table));
+ }
+ }
+}
+
+// ======================= Table ===============================
+
+/// Free a paimon_table.
+///
+/// # Safety
+/// Only call with a table returned from `paimon_catalog_get_table`.
+#[no_mangle]
+pub unsafe extern "C" fn paimon_table_free(table: *mut paimon_table) {
+ free_table_wrapper(table, |t| t.inner);
+}
+
+/// Create a new ReadBuilder from a Table.
+///
+/// # Safety
+/// `table` must be a valid pointer from `paimon_catalog_get_table`, or null
(returns error).
+#[no_mangle]
+pub unsafe extern "C" fn paimon_table_new_read_builder(
+ table: *const paimon_table,
+) -> paimon_result_read_builder {
+ if let Err(e) = check_non_null(table, "table") {
+ return paimon_result_read_builder {
+ read_builder: std::ptr::null_mut(),
+ error: e,
+ };
+ }
+ let table_ref = &*((*table).inner as *const Table);
+ paimon_result_read_builder {
+ read_builder: box_table_wrapper(table_ref, |inner| paimon_read_builder
{ inner }),
+ error: std::ptr::null_mut(),
+ }
+}
+
+// ======================= ReadBuilder ===============================
+
+/// Free a paimon_read_builder.
+///
+/// # Safety
+/// Only call with a read_builder returned from
`paimon_table_new_read_builder`.
+#[no_mangle]
+pub unsafe extern "C" fn paimon_read_builder_free(rb: *mut
paimon_read_builder) {
+ free_table_wrapper(rb, |r| r.inner);
+}
+
+/// Create a new TableScan from a ReadBuilder.
+///
+/// # Safety
+/// `rb` must be a valid pointer from `paimon_table_new_read_builder`, or null
(returns error).
+#[no_mangle]
+pub unsafe extern "C" fn paimon_read_builder_new_scan(
+ rb: *const paimon_read_builder,
+) -> paimon_result_table_scan {
+ if let Err(e) = check_non_null(rb, "rb") {
+ return paimon_result_table_scan {
+ scan: std::ptr::null_mut(),
+ error: e,
+ };
+ }
+ let table = &*((*rb).inner as *const Table);
+ paimon_result_table_scan {
+ scan: box_table_wrapper(table, |inner| paimon_table_scan { inner }),
+ error: std::ptr::null_mut(),
+ }
+}
+
+/// Create a new TableRead from a ReadBuilder.
+///
+/// # Safety
+/// `rb` must be a valid pointer from `paimon_table_new_read_builder`, or null
(returns error).
+#[no_mangle]
+pub unsafe extern "C" fn paimon_read_builder_new_read(
+ rb: *const paimon_read_builder,
+) -> paimon_result_new_read {
+ if let Err(e) = check_non_null(rb, "rb") {
+ return paimon_result_new_read {
+ read: std::ptr::null_mut(),
+ error: e,
+ };
+ }
+ let table = &*((*rb).inner as *const Table);
+ let rb_rust = table.new_read_builder();
+ match rb_rust.new_read() {
+ Ok(_) => {
+ let wrapper = box_table_wrapper(table, |inner| paimon_table_read {
inner });
+ paimon_result_new_read {
+ read: wrapper,
+ error: std::ptr::null_mut(),
+ }
+ }
+ Err(e) => paimon_result_new_read {
+ read: std::ptr::null_mut(),
+ error: paimon_error::from_paimon(e),
+ },
+ }
+}
+
+// ======================= TableScan ===============================
+
+/// Free a paimon_table_scan.
+///
+/// # Safety
+/// Only call with a scan returned from `paimon_read_builder_new_scan`.
+#[no_mangle]
+pub unsafe extern "C" fn paimon_table_scan_free(scan: *mut paimon_table_scan) {
+ free_table_wrapper(scan, |s| s.inner);
+}
+
+/// Execute a scan plan to get splits.
+///
+/// # Safety
+/// `scan` must be a valid pointer from `paimon_read_builder_new_scan`, or
null (returns error).
+#[no_mangle]
+pub unsafe extern "C" fn paimon_table_scan_plan(
+ scan: *const paimon_table_scan,
+) -> paimon_result_plan {
+ if let Err(e) = check_non_null(scan, "scan") {
+ return paimon_result_plan {
+ plan: std::ptr::null_mut(),
+ error: e,
+ };
+ }
+ let table = &*((*scan).inner as *const Table);
+ let rb = table.new_read_builder();
+ let table_scan = rb.new_scan();
+
+ match runtime().block_on(table_scan.plan()) {
+ Ok(plan) => {
+ let wrapper = Box::new(paimon_plan {
+ inner: Box::into_raw(Box::new(plan)) as *mut c_void,
+ });
+ paimon_result_plan {
+ plan: Box::into_raw(wrapper),
+ error: std::ptr::null_mut(),
+ }
+ }
+ Err(e) => paimon_result_plan {
+ plan: std::ptr::null_mut(),
+ error: paimon_error::from_paimon(e),
+ },
+ }
+}
+
+// ======================= Plan ===============================
+
+/// Free a paimon_plan.
+///
+/// # Safety
+/// Only call with a plan returned from `paimon_table_scan_plan`.
+#[no_mangle]
+pub unsafe extern "C" fn paimon_plan_free(plan: *mut paimon_plan) {
+ if !plan.is_null() {
+ let p = Box::from_raw(plan);
+ if !p.inner.is_null() {
+ drop(Box::from_raw(p.inner as *mut Plan));
+ }
+ }
+}
+
+// ======================= TableRead ===============================
+
+/// Free a paimon_table_read.
+///
+/// # Safety
+/// Only call with a read returned from `paimon_read_builder_new_read`.
+#[no_mangle]
+pub unsafe extern "C" fn paimon_table_read_free(read: *mut paimon_table_read) {
+ free_table_wrapper(read, |r| r.inner);
+}
+
+/// Read table data as Arrow record batches via a streaming reader.
+///
+/// Returns a `paimon_record_batch_reader` that yields one batch at a time
+/// via `paimon_record_batch_reader_next`. This avoids loading all batches
+/// into memory at once.
+///
+/// # Safety
+/// `read` and `plan` must be valid pointers from previous paimon C calls, or
null (returns error).
+#[no_mangle]
+pub unsafe extern "C" fn paimon_table_read_to_arrow(
+ read: *const paimon_table_read,
+ plan: *const paimon_plan,
+) -> paimon_result_record_batch_reader {
+ if let Err(e) = check_non_null(read, "read") {
+ return paimon_result_record_batch_reader {
+ reader: std::ptr::null_mut(),
+ error: e,
+ };
+ }
+ if let Err(e) = check_non_null(plan, "plan") {
+ return paimon_result_record_batch_reader {
+ reader: std::ptr::null_mut(),
+ error: e,
+ };
+ }
+
+ let table = &*((*read).inner as *const Table);
+ let plan_ref = &*((*plan).inner as *const Plan);
+
+ let rb = table.new_read_builder();
+ match rb.new_read() {
+ Ok(table_read) => match table_read.to_arrow(plan_ref.splits()) {
+ Ok(stream) => {
+ let reader = Box::new(stream);
+ let wrapper = Box::new(paimon_record_batch_reader {
+ inner: Box::into_raw(reader) as *mut c_void,
+ });
+ paimon_result_record_batch_reader {
+ reader: Box::into_raw(wrapper),
+ error: std::ptr::null_mut(),
+ }
+ }
+ Err(e) => paimon_result_record_batch_reader {
+ reader: std::ptr::null_mut(),
+ error: paimon_error::from_paimon(e),
+ },
+ },
+ Err(e) => paimon_result_record_batch_reader {
+ reader: std::ptr::null_mut(),
+ error: paimon_error::from_paimon(e),
+ },
+ }
+}
+
+// ======================= RecordBatchReader ===============================
+
+/// Get the next Arrow record batch from the reader.
+///
+/// When the stream is exhausted, both `batch.array` and `batch.schema` will
+/// be null. On error, `error` will be non-null.
+///
+/// After importing each batch, call `paimon_arrow_batch_free` to free the
+/// ArrowArray and ArrowSchema container structs.
+///
+/// # Safety
+/// `reader` must be a valid pointer from `paimon_table_read_to_arrow`, or
null (returns error).
+#[no_mangle]
+pub unsafe extern "C" fn paimon_record_batch_reader_next(
+ reader: *mut paimon_record_batch_reader,
+) -> paimon_result_next_batch {
+ if let Err(e) = check_non_null(reader, "reader") {
+ return paimon_result_next_batch {
+ batch: paimon_arrow_batch {
+ array: std::ptr::null_mut(),
+ schema: std::ptr::null_mut(),
+ },
+ error: e,
+ };
+ }
+
+ let stream = &mut *((*reader).inner as *mut ArrowRecordBatchStream);
+
+ match runtime().block_on(stream.next()) {
+ Some(Ok(batch)) => {
+ let schema = batch.schema();
+ let struct_array = StructArray::from(batch);
+ let ffi_array = FFI_ArrowArray::new(&struct_array.to_data());
+ let ffi_schema = match FFI_ArrowSchema::try_from(schema.as_ref()) {
+ Ok(s) => s,
+ Err(e) => {
+ return paimon_result_next_batch {
+ batch: paimon_arrow_batch {
+ array: std::ptr::null_mut(),
+ schema: std::ptr::null_mut(),
+ },
+ error:
paimon_error::from_paimon(paimon::Error::UnexpectedError {
+ message: format!("Failed to export Arrow schema:
{e}"),
+ source: Some(Box::new(e)),
+ }),
+ };
+ }
+ };
+
+ let array_ptr = Box::into_raw(Box::new(ffi_array)) as *mut c_void;
+ let schema_ptr = Box::into_raw(Box::new(ffi_schema)) as *mut
c_void;
+
+ paimon_result_next_batch {
+ batch: paimon_arrow_batch {
+ array: array_ptr,
+ schema: schema_ptr,
+ },
+ error: std::ptr::null_mut(),
+ }
+ }
+ Some(Err(e)) => paimon_result_next_batch {
+ batch: paimon_arrow_batch {
+ array: std::ptr::null_mut(),
+ schema: std::ptr::null_mut(),
+ },
+ error: paimon_error::from_paimon(e),
+ },
+ None => paimon_result_next_batch {
+ batch: paimon_arrow_batch {
+ array: std::ptr::null_mut(),
+ schema: std::ptr::null_mut(),
+ },
+ error: std::ptr::null_mut(),
+ },
+ }
+}
+
+/// Free a paimon_record_batch_reader.
+///
+/// # Safety
+/// Only call with a reader returned from `paimon_table_read_to_arrow`.
+#[no_mangle]
+pub unsafe extern "C" fn paimon_record_batch_reader_free(reader: *mut
paimon_record_batch_reader) {
+ if !reader.is_null() {
+ let wrapper = Box::from_raw(reader);
+ if !wrapper.inner.is_null() {
+ drop(Box::from_raw(wrapper.inner as *mut ArrowRecordBatchStream));
+ }
+ }
+}
+
+/// Free the ArrowArray and ArrowSchema container structs for a single batch.
+///
+/// # Safety
+/// `batch` must contain valid pointers returned by
`paimon_record_batch_reader_next`.
+#[no_mangle]
+pub unsafe extern "C" fn paimon_arrow_batch_free(batch: paimon_arrow_batch) {
+ if !batch.array.is_null() {
+ drop(Box::from_raw(batch.array as *mut FFI_ArrowArray));
+ }
+ if !batch.schema.is_null() {
+ drop(Box::from_raw(batch.schema as *mut FFI_ArrowSchema));
+ }
+}
diff --git a/bindings/c/src/types.rs b/bindings/c/src/types.rs
new file mode 100644
index 0000000..1cb2be7
--- /dev/null
+++ b/bindings/c/src/types.rs
@@ -0,0 +1,102 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::ffi::c_void;
+
+/// C-compatible byte buffer.
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct paimon_bytes {
+ pub data: *mut u8,
+ pub len: usize,
+}
+
+impl paimon_bytes {
+ pub fn new(v: Vec<u8>) -> Self {
+ let boxed = v.into_boxed_slice();
+ let len = boxed.len();
+ let data = Box::into_raw(boxed) as *mut u8;
+ Self { data, len }
+ }
+}
+
+/// Free a paimon_bytes buffer.
+///
+/// # Safety
+/// Only call with bytes returned from paimon C functions.
+#[no_mangle]
+pub unsafe extern "C" fn paimon_bytes_free(bytes: paimon_bytes) {
+ if !bytes.data.is_null() {
+ drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
+ bytes.data, bytes.len,
+ )));
+ }
+}
+
+/// Opaque wrapper around a heap-allocated Rust object.
+#[repr(C)]
+pub struct paimon_catalog {
+ pub inner: *mut c_void,
+}
+
+#[repr(C)]
+pub struct paimon_identifier {
+ pub inner: *mut c_void,
+}
+
+#[repr(C)]
+pub struct paimon_table {
+ pub inner: *mut c_void,
+}
+
+#[repr(C)]
+pub struct paimon_read_builder {
+ pub inner: *mut c_void,
+}
+
+#[repr(C)]
+pub struct paimon_table_scan {
+ pub inner: *mut c_void,
+}
+
+#[repr(C)]
+pub struct paimon_table_read {
+ pub inner: *mut c_void,
+}
+
+#[repr(C)]
+pub struct paimon_plan {
+ pub inner: *mut c_void,
+}
+
+#[repr(C)]
+pub struct paimon_record_batch_reader {
+ pub inner: *mut c_void,
+}
+
+/// A single Arrow record batch exported via the Arrow C Data Interface.
+///
+/// `array` and `schema` point to heap-allocated ArrowArray and ArrowSchema
+/// structs. After importing the data, call `paimon_arrow_batch_free` to free
+/// the container structs.
+#[repr(C)]
+pub struct paimon_arrow_batch {
+ /// Pointer to a heap-allocated ArrowArray.
+ pub array: *mut c_void,
+ /// Pointer to a heap-allocated ArrowSchema.
+ pub schema: *mut c_void,
+}