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,
+}

Reply via email to