This is an automated email from the ASF dual-hosted git repository.

JingsongLi 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 6555d4c3 feat(io): add optional OSS C++ SDK read backend (#1028)
6555d4c3 is described below

commit 6555d4c3a55a4ec86723faeeee29ecdcc5499c15
Author: XiaoHongbo <[email protected]>
AuthorDate: Sun Oct 4 22:20:10 2026 +0800

    feat(io): add optional OSS C++ SDK read backend (#1028)
---
 .github/workflows/ci.yml                     |  35 ++
 bindings/python/src/error.rs                 |  18 +
 crates/paimon/Cargo.toml                     |   2 +
 crates/paimon/src/error.rs                   |  48 +-
 crates/paimon/src/io/file_io.rs              |  73 +--
 crates/paimon/src/io/mod.rs                  |   5 +
 crates/paimon/src/io/storage.rs              |  40 ++
 crates/paimon/src/io/storage_oss_cpp.rs      | 780 +++++++++++++++++++++++++++
 crates/paimon/src/io/storage_oss_cpp_test.rs | 506 +++++++++++++++++
 docs/src/getting-started.md                  |   7 +
 integrations/oss-cpp/CMakeLists.txt          |  25 +
 integrations/oss-cpp/README.md               |  80 +++
 integrations/oss-cpp/bridge.cc               | 190 +++++++
 integrations/oss-cpp/bridge.h                |  65 +++
 14 files changed, 1819 insertions(+), 55 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 692a4680..3138b024 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -240,6 +240,10 @@ jobs:
             needs_docker: false
             needs_lumina: false
             needs_uv: false
+          - suite: oss-cpp
+            needs_docker: false
+            needs_lumina: false
+            needs_uv: false
     env:
       RUST_LOG: DEBUG
       RUST_BACKTRACE: full
@@ -338,3 +342,34 @@ jobs:
       - name: Stop Docker containers
         if: ${{ always() && matrix.needs_docker }}
         run: make docker-down
+
+      - name: Checkout OSS C++ SDK
+        if: matrix.suite == 'oss-cpp'
+        uses: actions/checkout@v7
+        with:
+          repository: aliyun/alibabacloud-oss-cpp-sdk-v2
+          ref: c13a82038b0696f99c32f4e86b623ffcc9e62140
+          path: .oss-cpp-sdk
+
+      - name: Build OSS C++ bridge
+        if: matrix.suite == 'oss-cpp'
+        run: |
+          sudo apt-get update
+          sudo apt-get install -y libcurl4-openssl-dev libssl-dev
+          cmake -S .oss-cpp-sdk -B build/oss-sdk -DCMAKE_BUILD_TYPE=Release \
+            -DUSE_SYSTEM_CURL=ON -DUSE_SYSTEM_OPENSSL=ON 
-DBUILD_SHARED_LIBS=ON \
+            -DCMAKE_CXX_FLAGS=-Wno-error=maybe-uninitialized \
+            -DCMAKE_INSTALL_PREFIX="$PWD/build/oss-install"
+          cmake --build build/oss-sdk -j 2
+          cmake --install build/oss-sdk
+          cmake -S integrations/oss-cpp -B build/oss-cpp \
+            -DCMAKE_PREFIX_PATH="$PWD/build/oss-install"
+          cmake --build build/oss-cpp -j 2
+          echo 
"OSS_CPP_BRIDGE_LIBRARY=$PWD/build/oss-cpp/liboss_cpp_bridge.so" >> 
"$GITHUB_ENV"
+
+      - name: OSS C++ SDK Integration Test
+        if: matrix.suite == 'oss-cpp'
+        run: |
+          cargo test --locked -p paimon --features 
storage-oss-cpp,storage-jindo --lib \
+            io::storage_oss_cpp -- --include-ignored
+          cargo clippy --locked -p paimon --features storage-oss-cpp --lib 
--tests -- -D warnings
diff --git a/bindings/python/src/error.rs b/bindings/python/src/error.rs
index b6ca355a..cb3199db 100644
--- a/bindings/python/src/error.rs
+++ b/bindings/python/src/error.rs
@@ -79,6 +79,24 @@ mod tests {
         });
     }
 
+    #[test]
+    fn oss_cpp_fork_error_has_distinct_python_type() {
+        Python::attach(|py| {
+            let error = paimon::Error::ProcessForkUnsupported {
+                message: "OSS C++ SDK cannot be used after fork; use spawn 
workers".to_string(),
+            };
+            assert!(to_py_err(error).is_instance_of::<ForkSafetyError>(py));
+
+            let error = datafusion::error::DataFusionError::External(Box::new(
+                paimon::Error::ProcessForkUnsupported {
+                    message: "OSS C++ SDK cannot be used after fork; use spawn 
workers".to_string(),
+                },
+            ))
+            .context("SQL planning failed");
+            assert!(df_to_py_err(error).is_instance_of::<ForkSafetyError>(py));
+        });
+    }
+
     #[test]
     fn wrapped_process_fork_error_has_distinct_python_type() {
         Python::attach(|py| {
diff --git a/crates/paimon/Cargo.toml b/crates/paimon/Cargo.toml
index 9ca18244..72eee590 100644
--- a/crates/paimon/Cargo.toml
+++ b/crates/paimon/Cargo.toml
@@ -42,6 +42,7 @@ storage-all = [
     "storage-gcs",
     "storage-hdfs",
     "storage-jindo",
+    "storage-oss-cpp",
 ]
 fulltext = ["dep:paimon-ftindex-core"]
 vortex = ["dep:vortex"]
@@ -63,6 +64,7 @@ storage-obs = ["dep:opendal-http-transport-reqwest", 
"dep:opendal-service-obs"]
 storage-gcs = ["dep:opendal-http-transport-reqwest", "dep:opendal-service-gcs"]
 storage-hdfs = ["dep:opendal-service-hdfs-native"]
 storage-jindo = ["storage-oss"]
+storage-oss-cpp = ["storage-oss"]
 
 [dependencies]
 rand = "0.8.5"
diff --git a/crates/paimon/src/error.rs b/crates/paimon/src/error.rs
index aa53a6dc..0087f16d 100644
--- a/crates/paimon/src/error.rs
+++ b/crates/paimon/src/error.rs
@@ -19,6 +19,8 @@ use snafu::prelude::*;
 
 pub(crate) const JINDO_FORK_ERROR: &str =
     "Jindo SDK cannot be reused after process fork; use spawn or avoid 
initializing Jindo in the parent process";
+pub(crate) const OSS_CPP_FORK_ERROR: &str =
+    "OSS C++ SDK cannot be used after fork; use spawn workers";
 
 /// Result type used in paimon.
 pub type Result<T, E = Error> = std::result::Result<T, E>;
@@ -150,8 +152,7 @@ impl Error {
         source: opendal::Error,
         message: impl Into<String>,
     ) -> Self {
-        if source.kind() == opendal::ErrorKind::Unsupported && 
source.message() == JINDO_FORK_ERROR
-        {
+        if is_native_fork_error(&source) {
             return Error::ProcessForkUnsupported {
                 message: source.message().to_string(),
             };
@@ -170,7 +171,7 @@ impl Error {
             Error::DataInvalid { source, .. } | Error::UnexpectedError { 
source, .. } => source
                 .as_deref()
                 .is_some_and(|source| 
error_chain_contains_process_fork(source)),
-            Error::IoUnexpected { source, .. } => is_jindo_fork_error(source),
+            Error::IoUnexpected { source, .. } => is_native_fork_error(source),
             Error::DataUnexpected { source, .. } => 
error_chain_contains_process_fork(source),
             Error::ParquetDataUnexpected { source, .. } => {
                 error_chain_contains_process_fork(source)
@@ -211,8 +212,9 @@ fn error_chain_contains_io_unexpected(error: &(dyn 
std::error::Error + 'static))
         .is_some_and(error_chain_contains_io_unexpected)
 }
 
-fn is_jindo_fork_error(error: &opendal::Error) -> bool {
-    error.kind() == opendal::ErrorKind::Unsupported && error.message() == 
JINDO_FORK_ERROR
+fn is_native_fork_error(error: &opendal::Error) -> bool {
+    error.kind() == opendal::ErrorKind::Unsupported
+        && matches!(error.message(), JINDO_FORK_ERROR | OSS_CPP_FORK_ERROR)
 }
 
 fn error_chain_contains_process_fork(error: &(dyn std::error::Error + 
'static)) -> bool {
@@ -220,7 +222,7 @@ fn error_chain_contains_process_fork(error: &(dyn 
std::error::Error + 'static))
         return error.is_process_fork_unsupported();
     }
     if let Some(error) = error.downcast_ref::<opendal::Error>() {
-        return is_jindo_fork_error(error);
+        return is_native_fork_error(error);
     }
     if let Some(error) = error.downcast_ref::<std::io::Error>() {
         if error
@@ -280,10 +282,14 @@ mod tests {
     use super::*;
 
     #[test]
-    fn preserves_jindo_fork_error_kind() {
+    fn preserves_native_fork_error_kind() {
+        for message in [JINDO_FORK_ERROR, OSS_CPP_FORK_ERROR] {
+            let error: Error = 
opendal::Error::new(opendal::ErrorKind::Unsupported, message).into();
+            assert!(matches!(error, Error::ProcessForkUnsupported { .. }));
+        }
         let error: Error =
-            opendal::Error::new(opendal::ErrorKind::Unsupported, 
JINDO_FORK_ERROR).into();
-        assert!(matches!(error, Error::ProcessForkUnsupported { .. }));
+            opendal::Error::new(opendal::ErrorKind::Unexpected, 
OSS_CPP_FORK_ERROR).into();
+        assert!(!error.is_process_fork_unsupported());
     }
 
     #[test]
@@ -309,17 +315,19 @@ mod tests {
 
     #[test]
     fn detects_wrapped_process_fork_error() {
-        let error = Error::UnexpectedError {
-            message: "outer context".to_string(),
-            source: Some(Box::new(Error::IoUnexpected {
-                message: "storage context".to_string(),
-                source: Box::new(opendal::Error::new(
-                    opendal::ErrorKind::Unsupported,
-                    JINDO_FORK_ERROR,
-                )),
-            })),
-        };
+        for message in [JINDO_FORK_ERROR, OSS_CPP_FORK_ERROR] {
+            let error = Error::UnexpectedError {
+                message: "outer context".to_string(),
+                source: Some(Box::new(Error::IoUnexpected {
+                    message: "storage context".to_string(),
+                    source: Box::new(opendal::Error::new(
+                        opendal::ErrorKind::Unsupported,
+                        message,
+                    )),
+                })),
+            };
 
-        assert!(error.is_process_fork_unsupported());
+            assert!(error.is_process_fork_unsupported());
+        }
     }
 }
diff --git a/crates/paimon/src/io/file_io.rs b/crates/paimon/src/io/file_io.rs
index e81af113..dbcebd60 100644
--- a/crates/paimon/src/io/file_io.rs
+++ b/crates/paimon/src/io/file_io.rs
@@ -1400,7 +1400,7 @@ mod file_action_test {
         async fn create(&self, _path: &str) -> crate::Result<(Operator, 
String)> {
             let service: Servicer = Arc::new(CountingListService {
                 pulls: Arc::clone(&self.pulls),
-                fail_with_fork_error: false,
+                fork_error: None,
             });
             Ok((
                 Operator::from_parts(OperationContext::default(), service),
@@ -1412,7 +1412,7 @@ mod file_action_test {
     #[derive(Debug)]
     struct CountingListService {
         pulls: Arc<AtomicUsize>,
-        fail_with_fork_error: bool,
+        fork_error: Option<&'static str>,
     }
 
     impl Service for CountingListService {
@@ -1450,8 +1450,8 @@ mod file_action_test {
             _path: &str,
             _args: OpStat,
         ) -> opendal::Result<RpStat> {
-            if self.fail_with_fork_error {
-                return Err(fork_test_error());
+            if let Some(message) = self.fork_error {
+                return Err(fork_test_error(message));
             }
             Err(unsupported_test_operation())
         }
@@ -1487,7 +1487,7 @@ mod file_action_test {
             Ok(CountingLister {
                 pulls: Arc::clone(&self.pulls),
                 next: 0,
-                fail_with_fork_error: self.fail_with_fork_error,
+                fork_error: self.fork_error,
             })
         }
 
@@ -1529,21 +1529,21 @@ mod file_action_test {
         )
     }
 
-    fn fork_test_error() -> opendal::Error {
-        opendal::Error::new(opendal::ErrorKind::Unsupported, JINDO_FORK_ERROR)
+    fn fork_test_error(message: &'static str) -> opendal::Error {
+        opendal::Error::new(opendal::ErrorKind::Unsupported, message)
     }
 
     struct CountingLister {
         pulls: Arc<AtomicUsize>,
         next: usize,
-        fail_with_fork_error: bool,
+        fork_error: Option<&'static str>,
     }
 
     impl oio::List for CountingLister {
         async fn next(&mut self) -> opendal::Result<Option<oio::Entry>> {
             self.pulls.fetch_add(1, AtomicOrdering::SeqCst);
-            if self.fail_with_fork_error {
-                return Err(fork_test_error());
+            if let Some(message) = self.fork_error {
+                return Err(fork_test_error(message));
             }
             if self.next == 0 {
                 self.next += 1;
@@ -1689,13 +1689,6 @@ mod file_action_test {
 
     #[tokio::test]
     async fn file_status_reads_preserve_fork_safety_error() {
-        let pulls = Arc::new(AtomicUsize::new(0));
-        let service: Servicer = Arc::new(CountingListService {
-            pulls,
-            fail_with_fork_error: true,
-        });
-        let operator = Operator::from_parts(OperationContext::default(), 
service);
-
         #[derive(Debug)]
         struct FixedProvider(Operator);
 
@@ -1709,26 +1702,36 @@ mod file_action_test {
             }
         }
 
-        let file_io = 
setup_memory_file_io().with_provider(Arc::new(FixedProvider(operator)));
-        let error = file_io
-            .get_status("counting:/objects/file")
-            .await
-            .unwrap_err();
-        assert!(
-            matches!(error, Error::ProcessForkUnsupported { .. }),
-            "{error:?}"
-        );
+        for message in [
+            crate::error::JINDO_FORK_ERROR,
+            crate::error::OSS_CPP_FORK_ERROR,
+        ] {
+            let service: Servicer = Arc::new(CountingListService {
+                pulls: Arc::new(AtomicUsize::new(0)),
+                fork_error: Some(message),
+            });
+            let operator = Operator::from_parts(OperationContext::default(), 
service);
+            let file_io = 
setup_memory_file_io().with_provider(Arc::new(FixedProvider(operator)));
+            let error = file_io
+                .get_status("counting:/objects/file")
+                .await
+                .unwrap_err();
+            assert!(
+                matches!(error, Error::ProcessForkUnsupported { .. }),
+                "{error:?}"
+            );
 
-        let error = 
file_io.list_status("counting:/objects/").await.unwrap_err();
-        assert!(matches!(error, Error::ProcessForkUnsupported { .. }));
+            let error = 
file_io.list_status("counting:/objects/").await.unwrap_err();
+            assert!(matches!(error, Error::ProcessForkUnsupported { .. }));
 
-        let snapshots =
-            crate::table::SnapshotManager::new(file_io, 
"counting:/objects/table".to_string());
-        let error = snapshots.get_latest_snapshot_id().await.unwrap_err();
-        assert!(
-            matches!(error, Error::ProcessForkUnsupported { .. }),
-            "{error:?}"
-        );
+            let snapshots =
+                crate::table::SnapshotManager::new(file_io, 
"counting:/objects/table".to_string());
+            let error = snapshots.get_latest_snapshot_id().await.unwrap_err();
+            assert!(
+                matches!(error, Error::ProcessForkUnsupported { .. }),
+                "{error:?}"
+            );
+        }
     }
 
     #[tokio::test]
diff --git a/crates/paimon/src/io/mod.rs b/crates/paimon/src/io/mod.rs
index 2718ab6b..c8dd33de 100644
--- a/crates/paimon/src/io/mod.rs
+++ b/crates/paimon/src/io/mod.rs
@@ -77,6 +77,11 @@ mod storage_jindo;
 #[cfg(feature = "storage-jindo")]
 use storage_jindo::*;
 
+#[cfg(feature = "storage-oss-cpp")]
+mod storage_oss_cpp;
+#[cfg(feature = "storage-oss-cpp")]
+use storage_oss_cpp::*;
+
 #[cfg(feature = "storage-s3")]
 mod storage_s3;
 #[cfg(feature = "storage-s3")]
diff --git a/crates/paimon/src/io/storage.rs b/crates/paimon/src/io/storage.rs
index afa9b25a..76ea3624 100644
--- a/crates/paimon/src/io/storage.rs
+++ b/crates/paimon/src/io/storage.rs
@@ -41,6 +41,8 @@ use std::sync::MutexGuard;
 use super::AzdlsStorageConfig;
 #[cfg(feature = "storage-jindo")]
 use super::JindoStorageConfig;
+#[cfg(feature = "storage-oss-cpp")]
+use super::OssCppStorageConfig;
 #[cfg(feature = "storage-oss")]
 use super::OssStorageConfig;
 use opendal::Operator;
@@ -70,6 +72,11 @@ use super::FileIOBuilder;
 /// The storage carries all supported storage services in paimon
 #[derive(Debug)]
 pub enum Storage {
+    #[cfg(feature = "storage-oss-cpp")]
+    OssCpp {
+        config: Box<OssCppStorageConfig>,
+        operators: Mutex<HashMap<String, Operator>>,
+    },
     /// A caller-provided opendal operator, explicitly scoped to filesystem 
semantics
     /// (see `FileIOBuilder::with_fs_operator`).
     CustomFs { op: Operator },
@@ -142,6 +149,20 @@ impl Storage {
             }),
             #[cfg(feature = "storage-oss")]
             "oss" => {
+                if props
+                    .get("fs.oss.impl")
+                    .is_some_and(|v| v.eq_ignore_ascii_case("cpp"))
+                {
+                    #[cfg(feature = "storage-oss-cpp")]
+                    return Ok(Self::OssCpp {
+                        config: Box::new(super::oss_cpp_config_parse(props)?),
+                        operators: Mutex::new(HashMap::new()),
+                    });
+                    #[cfg(not(feature = "storage-oss-cpp"))]
+                    return Err(error::Error::IoUnsupported {
+                        message: "OSS C++ SDK requires the storage-oss-cpp 
feature".to_string(),
+                    });
+                }
                 #[cfg(feature = "storage-jindo")]
                 if super::use_jindo(&props)? {
                     let config = super::jindo_config_parse(props)?;
@@ -260,6 +281,15 @@ impl Storage {
                 let op = Self::cached_oss_operator(config, operators, path, 
&bucket)?;
                 Ok((op, Cow::Borrowed(relative_path)))
             }
+            #[cfg(feature = "storage-oss-cpp")]
+            Storage::OssCpp { config, operators } => {
+                let (bucket, relative_path) =
+                    Self::bucket_and_relative_path(path, "OSS C++", &["oss"])?;
+                let op = Self::cached_operator(operators, "OSS C++", &bucket, 
|| {
+                    super::oss_cpp_config_build(config, &bucket)
+                })?;
+                Ok((op, Cow::Borrowed(relative_path)))
+            }
             #[cfg(feature = "storage-jindo")]
             Storage::Jindo { config, operators } => {
                 let (bucket, relative_path) =
@@ -693,6 +723,16 @@ mod fs_relative_path_tests {
 mod tests {
     use super::*;
 
+    #[cfg(all(feature = "storage-oss", not(feature = "storage-oss-cpp")))]
+    #[test]
+    fn oss_cpp_without_feature_does_not_fall_back() {
+        let result = 
Storage::build(FileIOBuilder::new("oss").with_props(HashMap::from([(
+            "fs.oss.impl".to_string(),
+            "cpp".to_string(),
+        )])));
+        assert!(matches!(result, Err(error::Error::IoUnsupported { .. })));
+    }
+
     fn memory_operator() -> Operator {
         
Operator::from_config(opendal::services::MemoryConfig::default()).unwrap()
     }
diff --git a/crates/paimon/src/io/storage_oss_cpp.rs 
b/crates/paimon/src/io/storage_oss_cpp.rs
new file mode 100644
index 00000000..c0ecb8dd
--- /dev/null
+++ b/crates/paimon/src/io/storage_oss_cpp.rs
@@ -0,0 +1,780 @@
+// 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::collections::HashMap;
+use std::ffi::{c_char, c_void, CStr, CString};
+use std::fmt::{Debug, Formatter};
+use std::sync::atomic::{AtomicU32, Ordering};
+use std::sync::Arc;
+
+use libloading::Library;
+use opendal::raw::*;
+use opendal::{Buffer, Builder, BytesRange, Capability, EntryMode, Error, 
ErrorKind, Metadata};
+use opendal::{OperationContext, Operator, Result};
+use tokio::sync::{OnceCell, Semaphore};
+
+const PREFIX: &str = "fs.oss.cpp.";
+const DEFAULT_CONCURRENCY: usize = 8;
+
+#[derive(Clone)]
+pub struct OssCppStorageConfig {
+    library: String,
+    strings: Vec<CString>,
+    concurrency: usize,
+    gate: Arc<Semaphore>,
+    connect_timeout: i64,
+    request_timeout: i64,
+    retry_attempts: i64,
+    path_style: u8,
+}
+
+impl Default for OssCppStorageConfig {
+    fn default() -> Self {
+        Self {
+            library: String::new(),
+            strings: Vec::new(),
+            concurrency: 0,
+            gate: Arc::new(Semaphore::new(0)),
+            connect_timeout: 0,
+            request_timeout: 0,
+            retry_attempts: 0,
+            path_style: 0,
+        }
+    }
+}
+
+impl Debug for OssCppStorageConfig {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("OssCppStorageConfig")
+            .field("library", &self.library)
+            .field("concurrency", &self.concurrency)
+            .finish_non_exhaustive()
+    }
+}
+
+pub(crate) fn oss_cpp_config_parse(
+    props: HashMap<String, String>,
+) -> crate::Result<OssCppStorageConfig> {
+    let parse = || -> Result<OssCppStorageConfig> {
+        let required = |key: &str| -> Result<String> {
+            props
+                .get(key)
+                .filter(|s| !s.is_empty())
+                .cloned()
+                .ok_or_else(|| Error::new(ErrorKind::ConfigInvalid, 
format!("Missing {key}")))
+        };
+        let number = |name: &str, default: i64| -> Result<i64> {
+            let key = format!("{PREFIX}{name}");
+            match props.get(&key) {
+                None => Ok(default),
+                Some(value) => value.parse::<i64>().ok().filter(|n| *n > 
0).ok_or_else(|| {
+                    Error::new(ErrorKind::ConfigInvalid, format!("{key} must 
be positive"))
+                }),
+            }
+        };
+        let endpoint = required("fs.oss.endpoint")?;
+        let endpoint = if endpoint.contains("://") {
+            endpoint
+        } else {
+            format!("https://{endpoint}";)
+        };
+        let url = url::Url::parse(&endpoint)
+            .map_err(|_| Error::new(ErrorKind::ConfigInvalid, "Invalid OSS 
endpoint"))?;
+        if !matches!(url.scheme(), "http" | "https")
+            || url.host_str().is_none()
+            || !url.username().is_empty()
+            || url.password().is_some()
+            || url.query().is_some()
+            || url.fragment().is_some()
+            || url.path() != "/"
+        {
+            return Err(Error::new(
+                ErrorKind::ConfigInvalid,
+                "OSS endpoint must be an HTTP(S) origin",
+            ));
+        }
+        let strings = [
+            endpoint,
+            required("fs.oss.region")?,
+            required("fs.oss.accessKeyId")?,
+            required("fs.oss.accessKeySecret")?,
+            props
+                .get("fs.oss.securityToken")
+                .cloned()
+                .unwrap_or_default(),
+            // The native SDK appends this to its own User-Agent.
+            format!("{} oss-cpp", super::user_agent::oss_user_agent(&props)),
+        ]
+        .into_iter()
+        .map(|s| cstring(&s))
+        .collect::<Result<Vec<_>>>()?;
+        let concurrency = number("max.concurrent.requests", 
DEFAULT_CONCURRENCY as i64)? as usize;
+        if concurrency > Semaphore::MAX_PERMITS {
+            return Err(Error::new(
+                ErrorKind::ConfigInvalid,
+                "OSS C++ concurrency exceeds semaphore limit",
+            ));
+        }
+        let path_style = match 
props.get("fs.oss.cpp.path-style").map(String::as_str) {
+            None | Some("false") => 0,
+            Some("true") => 1,
+            _ => {
+                return Err(Error::new(
+                    ErrorKind::ConfigInvalid,
+                    "Invalid OSS C++ path-style",
+                ))
+            }
+        };
+        Ok(OssCppStorageConfig {
+            library: required("fs.oss.cpp.library.path")?,
+            strings,
+            concurrency,
+            gate: Arc::new(Semaphore::new(concurrency)),
+            connect_timeout: number("connect.timeout-ms", 10_000)?,
+            request_timeout: number("request.timeout-ms", 30_000)?,
+            retry_attempts: number("retry.max-attempts", 3)?,
+            path_style,
+        })
+    };
+    parse().map_err(|e| crate::Error::ConfigInvalid {
+        message: e.to_string(),
+    })
+}
+
+pub(crate) fn oss_cpp_config_build(
+    config: &OssCppStorageConfig,
+    bucket: &str,
+) -> crate::Result<Operator> {
+    Operator::new(CppBuilder {
+        config: config.clone(),
+        bucket: bucket.to_string(),
+    })
+    .map_err(|e| crate::Error::IoUnexpected {
+        message: "Cannot build OSS C++ operator".to_string(),
+        source: Box::new(e),
+    })
+}
+
+#[derive(Default)]
+struct CppBuilder {
+    config: OssCppStorageConfig,
+    bucket: String,
+}
+
+impl Builder for CppBuilder {
+    type Config = ();
+    fn build(self) -> Result<impl Service> {
+        if self.config.strings.len() != 6 || self.config.concurrency == 0 {
+            return Err(Error::new(
+                ErrorKind::ConfigInvalid,
+                "Missing OSS C++ configuration",
+            ));
+        }
+        Ok(CppService {
+            info: ServiceInfo::new("oss-cpp", "/", &self.bucket),
+            client: Arc::new(LazyClient {
+                gate: self.config.gate.clone(),
+                config: self.config,
+                bucket: self.bucket,
+                client: OnceCell::new(),
+            }),
+        })
+    }
+}
+
+struct CppService {
+    info: ServiceInfo,
+    client: Arc<LazyClient>,
+}
+
+impl Debug for CppService {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("OssCppService")
+            .field("info", &self.info)
+            .finish()
+    }
+}
+
+impl Service for CppService {
+    type Reader = CppReader;
+    type Writer = ();
+    type Lister = CppLister;
+    type Deleter = ();
+    type Copier = ();
+
+    fn info(&self) -> ServiceInfo {
+        self.info.clone()
+    }
+    fn capability(&self) -> Capability {
+        Capability {
+            stat: true,
+            read: true,
+            read_with_suffix: true,
+            list: true,
+            list_with_recursive: true,
+            shared: true,
+            ..Default::default()
+        }
+    }
+    async fn stat(&self, _: &OperationContext, path: &str, _: OpStat) -> 
Result<RpStat> {
+        let path = path.to_string();
+        let metadata = self.client.call(move |c| c.stat(&path)).await?;
+        Ok(RpStat::new(metadata))
+    }
+    fn read(&self, _: &OperationContext, path: &str, _: OpRead) -> 
Result<Self::Reader> {
+        Ok(CppReader {
+            client: self.client.clone(),
+            path: path.to_string(),
+        })
+    }
+    fn list(&self, _: &OperationContext, path: &str, args: OpList) -> 
Result<Self::Lister> {
+        Ok(CppLister {
+            client: self.client.clone(),
+            path: path.to_string(),
+            recursive: args.recursive(),
+            max_keys: args.limit().unwrap_or(1000).clamp(1, 1000) as u32,
+            token: String::new(),
+            done: false,
+            entries: Vec::new().into_iter(),
+        })
+    }
+    async fn create_dir(
+        &self,
+        _: &OperationContext,
+        _: &str,
+        _: OpCreateDir,
+    ) -> Result<RpCreateDir> {
+        unsupported()
+    }
+    fn write(&self, _: &OperationContext, _: &str, _: OpWrite) -> 
Result<Self::Writer> {
+        unsupported()
+    }
+    fn delete(&self, _: &OperationContext) -> Result<Self::Deleter> {
+        unsupported()
+    }
+    fn copy(
+        &self,
+        _: &OperationContext,
+        _: &str,
+        _: &str,
+        _: OpCopy,
+        _: OpCopier,
+    ) -> Result<Self::Copier> {
+        unsupported()
+    }
+    async fn rename(
+        &self,
+        _: &OperationContext,
+        _: &str,
+        _: &str,
+        _: OpRename,
+    ) -> Result<RpRename> {
+        unsupported()
+    }
+    async fn presign(&self, _: &OperationContext, _: &str, _: OpPresign) -> 
Result<RpPresign> {
+        unsupported()
+    }
+}
+
+fn unsupported<T>() -> Result<T> {
+    Err(Error::new(
+        ErrorKind::Unsupported,
+        "OSS C++ backend is read-only",
+    ))
+}
+
+struct CppReader {
+    client: Arc<LazyClient>,
+    path: String,
+}
+
+impl oio::Read for CppReader {
+    async fn open(&self, range: BytesRange) -> Result<(RpRead, Box<dyn 
oio::ReadStreamDyn>)> {
+        let (_, buffer) = self.read(range).await?;
+        Ok((RpRead::default(), Box::new(buffer)))
+    }
+    async fn read(&self, range: BytesRange) -> Result<(RpRead, Buffer)> {
+        let path = self.path.clone();
+        let data = self.client.call(move |c| c.read(&path, range)).await?;
+        Ok((RpRead::default(), Buffer::from(data)))
+    }
+}
+
+struct CppLister {
+    client: Arc<LazyClient>,
+    path: String,
+    recursive: bool,
+    max_keys: u32,
+    token: String,
+    done: bool,
+    entries: std::vec::IntoIter<oio::Entry>,
+}
+
+impl oio::List for CppLister {
+    async fn next(&mut self) -> Result<Option<oio::Entry>> {
+        loop {
+            if let Some(entry) = self.entries.next() {
+                return Ok(Some(entry));
+            }
+            if self.done {
+                return Ok(None);
+            }
+            let path = self.path.clone();
+            let token = self.token.clone();
+            let recursive = self.recursive;
+            let max_keys = self.max_keys;
+            let (entries, next) = self
+                .client
+                .call(move |c| c.list(&path, &token, recursive, max_keys))
+                .await?;
+            self.done = next.is_empty();
+            self.token = next;
+            self.entries = entries.into_iter();
+        }
+    }
+}
+
+struct LazyClient {
+    config: OssCppStorageConfig,
+    bucket: String,
+    client: OnceCell<Arc<Client>>,
+    gate: Arc<Semaphore>,
+}
+
+// Native clients/connection pools cannot be inherited across fork. Check 
before
+// touching OnceCell or Semaphore: either may have been locked by a parent 
thread.
+fn ensure_process() -> Result<()> {
+    static PID: AtomicU32 = AtomicU32::new(0);
+    let current = std::process::id();
+    match PID.compare_exchange(0, current, Ordering::AcqRel, 
Ordering::Acquire) {
+        Ok(_) => Ok(()),
+        Err(pid) if pid == current => Ok(()),
+        Err(_) => Err(Error::new(
+            ErrorKind::Unsupported,
+            crate::error::OSS_CPP_FORK_ERROR,
+        )),
+    }
+}
+
+impl LazyClient {
+    async fn call<T: Send + 'static>(
+        &self,
+        operation: impl FnOnce(&Client) -> Result<T> + Send + 'static,
+    ) -> Result<T> {
+        ensure_process()?;
+        let client = self
+            .client
+            .get_or_try_init(|| async {
+                let config = self.config.clone();
+                let bucket = self.bucket.clone();
+                blocking(move || Client::new(&config, 
&bucket).map(Arc::new)).await
+            })
+            .await?
+            .clone();
+        let permit = self
+            .gate
+            .clone()
+            .acquire_owned()
+            .await
+            .map_err(|_| Error::new(ErrorKind::Unexpected, "OSS C++ request 
gate closed"))?;
+        blocking(move || {
+            // Cancellation does not stop a synchronous C++ request. Keep both
+            // the client and permit alive until the actual native call 
returns.
+            let _permit = permit;
+            operation(&client)
+        })
+        .await
+    }
+}
+
+async fn blocking<T: Send + 'static>(f: impl FnOnce() -> Result<T> + Send + 
'static) -> Result<T> {
+    tokio::task::spawn_blocking(f)
+        .await
+        .map_err(|_| Error::new(ErrorKind::Unexpected, "OSS C++ blocking task 
failed"))?
+}
+
+#[repr(C)]
+struct NativeConfig {
+    endpoint: *const c_char,
+    region: *const c_char,
+    access_key: *const c_char,
+    secret_key: *const c_char,
+    token: *const c_char,
+    user_agent: *const c_char,
+    connect_timeout_ms: i64,
+    request_timeout_ms: i64,
+    retry_attempts: i64,
+    path_style: u8,
+}
+
+#[repr(C)]
+struct NativeError {
+    status: i32,
+    code: [c_char; 128],
+    request_id: [c_char; 256],
+}
+impl Default for NativeError {
+    fn default() -> Self {
+        Self {
+            status: 0,
+            code: [0; 128],
+            request_id: [0; 256],
+        }
+    }
+}
+impl NativeError {
+    fn check(&self, status: i32) -> Result<()> {
+        if status == 0 {
+            return Ok(());
+        }
+        let kind = match self.status {
+            404 => ErrorKind::NotFound,
+            401 | 403 => ErrorKind::PermissionDenied,
+            429 | 503 => ErrorKind::RateLimited,
+            416 => ErrorKind::RangeNotSatisfied,
+            _ => ErrorKind::Unexpected,
+        };
+        // Only expose status, symbolic code and request ID, never request 
URLs,
+        // authorization headers or SDK exception text.
+        Err(Error::new(kind, "OSS C++ operation failed")
+            .with_context("status", self.status.to_string())
+            .with_context("code", native_string(self.code.as_ptr()))
+            .with_context("request_id", 
native_string(self.request_id.as_ptr())))
+    }
+}
+
+#[repr(C)]
+struct NativeMetadata {
+    size: i64,
+    modified: [c_char; 64],
+}
+type EntryCallback =
+    unsafe extern "C" fn(*mut c_void, *const c_char, usize, i64, *const 
c_char, u8);
+type Create = unsafe extern "C" fn(*const NativeConfig, *mut NativeError) -> 
*mut c_void;
+type Destroy = unsafe extern "C" fn(*mut c_void);
+type Stat = unsafe extern "C" fn(
+    *mut c_void,
+    *const c_char,
+    *const c_char,
+    *mut NativeMetadata,
+    *mut NativeError,
+) -> i32;
+type Read = unsafe extern "C" fn(
+    *mut c_void,
+    *const c_char,
+    *const c_char,
+    u64,
+    usize,
+    *mut u8,
+    *mut NativeError,
+) -> i32;
+type List = unsafe extern "C" fn(
+    *mut c_void,
+    *const c_char,
+    *const c_char,
+    *const c_char,
+    u8,
+    u32,
+    EntryCallback,
+    *mut c_void,
+    *mut *mut c_char,
+    *mut NativeError,
+) -> i32;
+type FreeString = unsafe extern "C" fn(*mut c_char);
+
+struct Api {
+    create: Create,
+    destroy: Destroy,
+    stat: Stat,
+    read: Read,
+    list: List,
+    free_string: FreeString,
+    _library: Library,
+}
+impl Api {
+    fn load(path: &str) -> Result<Self> {
+        // SAFETY: loading native code is explicit opt-in via a trusted library
+        // path. Symbols follow bridge.h; retain the library for their 
lifetime.
+        unsafe {
+            let library = Library::new(path).map_err(|_| {
+                Error::new(
+                    ErrorKind::ConfigInvalid,
+                    "Cannot load fs.oss.cpp.library.path or its dependencies",
+                )
+            })?;
+            let load_error = || Error::new(ErrorKind::ConfigInvalid, "Invalid 
OSS C++ bridge ABI");
+            let version = library
+                .get::<unsafe extern "C" fn() -> 
u32>(b"oss_cpp_bridge_abi_version\0")
+                .map_err(|_| load_error())?;
+            if version() != 2 {
+                return Err(load_error());
+            }
+            Ok(Self {
+                create: *library
+                    .get(b"oss_cpp_bridge_create\0")
+                    .map_err(|_| load_error())?,
+                destroy: *library
+                    .get(b"oss_cpp_bridge_destroy\0")
+                    .map_err(|_| load_error())?,
+                stat: *library
+                    .get(b"oss_cpp_bridge_stat\0")
+                    .map_err(|_| load_error())?,
+                read: *library
+                    .get(b"oss_cpp_bridge_read\0")
+                    .map_err(|_| load_error())?,
+                list: *library
+                    .get(b"oss_cpp_bridge_list\0")
+                    .map_err(|_| load_error())?,
+                free_string: *library
+                    .get(b"oss_cpp_bridge_free_string\0")
+                    .map_err(|_| load_error())?,
+                _library: library,
+            })
+        }
+    }
+}
+
+struct Client {
+    api: Arc<Api>,
+    pointer: *mut c_void,
+    bucket: CString,
+    pid: u32,
+}
+// SAFETY: the bridge uses a concurrent OSSClient and per-call outputs. The
+// immutable client is released only after all blocking calls drop their Arc.
+unsafe impl Send for Client {}
+unsafe impl Sync for Client {}
+
+impl Client {
+    fn new(config: &OssCppStorageConfig, bucket: &str) -> Result<Self> {
+        let bucket = cstring(bucket)?;
+        let api = Arc::new(Api::load(&config.library)?);
+        let s = &config.strings;
+        let input = NativeConfig {
+            endpoint: s[0].as_ptr(),
+            region: s[1].as_ptr(),
+            access_key: s[2].as_ptr(),
+            secret_key: s[3].as_ptr(),
+            token: s[4].as_ptr(),
+            user_agent: s[5].as_ptr(),
+            connect_timeout_ms: config.connect_timeout,
+            request_timeout_ms: config.request_timeout,
+            retry_attempts: config.retry_attempts,
+            path_style: config.path_style,
+        };
+        let mut error = NativeError::default();
+        let pointer = unsafe { (api.create)(&input, &mut error) };
+        if pointer.is_null() {
+            error.check(-1)?;
+            unreachable!();
+        }
+        Ok(Self {
+            api,
+            pointer,
+            bucket,
+            pid: std::process::id(),
+        })
+    }
+    fn stat(&self, key: &str) -> Result<Metadata> {
+        if key.is_empty() {
+            return Ok(Metadata::new(EntryMode::DIR));
+        }
+        if key.ends_with('/') {
+            let (entries, _) = self.list(key, "", true, 1)?;
+            return if entries.is_empty() {
+                Err(Error::new(ErrorKind::NotFound, "OSS directory not found"))
+            } else {
+                Ok(Metadata::new(EntryMode::DIR))
+            };
+        }
+        let key = cstring(key)?;
+        let mut out = NativeMetadata {
+            size: 0,
+            modified: [0; 64],
+        };
+        let mut error = NativeError::default();
+        let status = unsafe {
+            (self.api.stat)(
+                self.pointer,
+                self.bucket.as_ptr(),
+                key.as_ptr(),
+                &mut out,
+                &mut error,
+            )
+        };
+        error.check(status)?;
+        metadata(out.size, &native_string(out.modified.as_ptr()), false)
+    }
+    fn read(&self, key: &str, range: BytesRange) -> Result<Vec<u8>> {
+        let (offset, length) = match range {
+            BytesRange::Range {
+                offset,
+                size: Some(length),
+            } => (offset, length),
+            BytesRange::Range { offset, size: None } => {
+                let size = self.stat(key)?.content_length();
+                let length = size.checked_sub(offset).ok_or_else(|| {
+                    Error::new(ErrorKind::RangeNotSatisfied, "Range starts 
after EOF")
+                })?;
+                (offset, length)
+            }
+            BytesRange::Suffix { size } => {
+                let length = self.stat(key)?.content_length();
+                (length.saturating_sub(size), size.min(length))
+            }
+        };
+        offset
+            .checked_add(length)
+            .ok_or_else(|| Error::new(ErrorKind::RangeNotSatisfied, "Range 
overflow"))?;
+        let length = usize::try_from(length)
+            .map_err(|_| Error::new(ErrorKind::RangeNotSatisfied, "Range too 
large"))?;
+        if length == 0 {
+            return Ok(Vec::new());
+        }
+        if length > i64::MAX as usize {
+            return Err(Error::new(ErrorKind::RangeNotSatisfied, "Range too 
large"));
+        }
+        let key = cstring(key)?;
+        let mut data = Vec::new();
+        data.try_reserve_exact(length)
+            .map_err(|_| Error::new(ErrorKind::Unexpected, "Cannot allocate 
read buffer"))?;
+        data.resize(length, 0);
+        let mut error = NativeError::default();
+        let status = unsafe {
+            (self.api.read)(
+                self.pointer,
+                self.bucket.as_ptr(),
+                key.as_ptr(),
+                offset,
+                length,
+                data.as_mut_ptr(),
+                &mut error,
+            )
+        };
+        error.check(status)?;
+        Ok(data)
+    }
+    fn list(
+        &self,
+        prefix: &str,
+        token: &str,
+        recursive: bool,
+        max_keys: u32,
+    ) -> Result<(Vec<oio::Entry>, String)> {
+        let prefix = cstring(prefix)?;
+        let token = cstring(token)?;
+        let mut entries = EntryCollector {
+            entries: Vec::new(),
+            error: None,
+        };
+        let mut next = std::ptr::null_mut();
+        let mut error = NativeError::default();
+        let status = unsafe {
+            (self.api.list)(
+                self.pointer,
+                self.bucket.as_ptr(),
+                prefix.as_ptr(),
+                token.as_ptr(),
+                u8::from(recursive),
+                max_keys,
+                collect_entry,
+                &mut entries as *mut EntryCollector as *mut c_void,
+                &mut next,
+                &mut error,
+            )
+        };
+        let next_token = if next.is_null() {
+            String::new()
+        } else {
+            let value = native_string(next);
+            unsafe { (self.api.free_string)(next) };
+            value
+        };
+        error.check(status)?;
+        if let Some(error) = entries.error {
+            return Err(error);
+        }
+        Ok((entries.entries, next_token))
+    }
+}
+impl Drop for Client {
+    fn drop(&mut self) {
+        if self.pid != std::process::id() {
+            // Do not destroy native handles or unload their library in a 
forked child.
+            std::mem::forget(self.api.clone());
+            return;
+        }
+        unsafe { (self.api.destroy)(self.pointer) };
+    }
+}
+
+struct EntryCollector {
+    entries: Vec<oio::Entry>,
+    error: Option<Error>,
+}
+unsafe extern "C" fn collect_entry(
+    ctx: *mut c_void,
+    key: *const c_char,
+    len: usize,
+    size: i64,
+    modified: *const c_char,
+    dir: u8,
+) {
+    // SAFETY: the synchronous bridge only invokes this callback while ctx and
+    // the borrowed native strings are alive; no callback is retained.
+    let collector = unsafe { &mut *(ctx as *mut EntryCollector) };
+    if collector.error.is_some() {
+        return;
+    }
+    let bytes = unsafe { std::slice::from_raw_parts(key.cast::<u8>(), len) };
+    let result = std::str::from_utf8(bytes)
+        .map_err(|_| Error::new(ErrorKind::Unexpected, "Invalid UTF-8 object 
key"))
+        .and_then(|path| {
+            metadata(size, &native_string(modified), dir != 0)
+                .map(|meta| oio::Entry::new(path, meta))
+        });
+    match result {
+        Ok(entry) => collector.entries.push(entry),
+        Err(error) => collector.error = Some(error),
+    }
+}
+fn metadata(size: i64, modified: &str, dir: bool) -> Result<Metadata> {
+    let size = u64::try_from(size)
+        .map_err(|_| Error::new(ErrorKind::Unexpected, "Negative object 
size"))?;
+    let mut meta = Metadata::new(if dir { EntryMode::DIR } else { 
EntryMode::FILE });
+    meta.set_content_length(size);
+    if !modified.is_empty() {
+        let time = chrono::DateTime::parse_from_rfc2822(modified)
+            .or_else(|_| chrono::DateTime::parse_from_rfc3339(modified))
+            .map_err(|_| Error::new(ErrorKind::Unexpected, "Invalid object 
modification time"))?;
+        
meta.set_last_modified(Timestamp::from_millisecond(time.timestamp_millis())?);
+    }
+    Ok(meta)
+}
+fn cstring(value: &str) -> Result<CString> {
+    CString::new(value).map_err(|_| Error::new(ErrorKind::ConfigInvalid, "NUL 
in OSS C++ input"))
+}
+fn native_string(value: *const c_char) -> String {
+    // All native outputs are NUL terminated by bridge.cc.
+    unsafe { CStr::from_ptr(value) }
+        .to_string_lossy()
+        .into_owned()
+}
+
+#[cfg(test)]
+#[path = "storage_oss_cpp_test.rs"]
+mod tests;
diff --git a/crates/paimon/src/io/storage_oss_cpp_test.rs 
b/crates/paimon/src/io/storage_oss_cpp_test.rs
new file mode 100644
index 00000000..21085258
--- /dev/null
+++ b/crates/paimon/src/io/storage_oss_cpp_test.rs
@@ -0,0 +1,506 @@
+// 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 super::*;
+use crate::io::FileRead;
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::sync::Mutex;
+use std::time::Duration;
+
+use axum::body::Body;
+use axum::extract::{Query, State};
+use axum::http::{HeaderMap, Method, Response, StatusCode, Uri};
+use axum::Router;
+
+fn props() -> HashMap<String, String> {
+    [
+        ("fs.oss.impl", "cpp"),
+        ("fs.oss.cpp.library.path", "/not/a/real/library.so"),
+        ("fs.oss.endpoint", "oss-cn-hangzhou.aliyuncs.com"),
+        ("fs.oss.region", "cn-hangzhou"),
+        ("fs.oss.accessKeyId", "test-ak"),
+        ("fs.oss.accessKeySecret", "test-secret"),
+        ("fs.oss.securityToken", "test-sts"),
+    ]
+    .into_iter()
+    .map(|(k, v)| (k.to_string(), v.to_string()))
+    .collect()
+}
+
+#[test]
+fn config_defaults_validation_and_redaction() {
+    let config = oss_cpp_config_parse(props()).unwrap();
+    assert_eq!(config.concurrency, 8);
+    assert_eq!(config.retry_attempts, 3);
+    assert_eq!(
+        config.strings[0].to_str().unwrap(),
+        "https://oss-cn-hangzhou.aliyuncs.com";
+    );
+    let debug = format!("{config:?}");
+    assert!(!debug.contains("test-secret"));
+    assert!(!debug.contains("test-sts"));
+    for (key, value) in [
+        ("fs.oss.cpp.max.concurrent.requests", "0"),
+        ("fs.oss.cpp.max.concurrent.requests", "-1"),
+        ("fs.oss.cpp.request.timeout-ms", "bad"),
+        ("fs.oss.cpp.retry.max-attempts", "0"),
+        ("fs.oss.cpp.path-style", "yes"),
+        ("fs.oss.endpoint", "ftp://host";),
+        ("fs.oss.endpoint", "https://secret@host";),
+        ("fs.oss.endpoint", "https://host/path";),
+        ("fs.oss.accessKeySecret", "nul\0secret"),
+    ] {
+        let mut options = props();
+        options.insert(key.to_string(), value.to_string());
+        assert!(oss_cpp_config_parse(options).is_err(), "{key}");
+    }
+    for key in [
+        "fs.oss.region",
+        "fs.oss.endpoint",
+        "fs.oss.cpp.library.path",
+    ] {
+        let mut options = props();
+        options.remove(key);
+        assert!(oss_cpp_config_parse(options).is_err(), "{key}");
+    }
+}
+
+#[tokio::test]
+async fn selection_is_explicit_and_missing_library_fails() {
+    let file_io = crate::io::FileIOBuilder::new("oss")
+        .with_props(props())
+        .build()
+        .unwrap();
+    assert!(file_io.get_status("oss://bucket/object").await.is_err());
+    let config = oss_cpp_config_parse(props()).unwrap();
+    let op = oss_cpp_config_build(&config, "bucket").unwrap();
+    assert_eq!(op.info().scheme(), "oss-cpp");
+    assert_eq!(
+        op.write("object", "data").await.unwrap_err().kind(),
+        ErrorKind::Unsupported
+    );
+    assert_eq!(
+        op.delete("object").await.unwrap_err().kind(),
+        ErrorKind::Unsupported
+    );
+}
+
+#[test]
+fn malformed_metadata_and_error_mapping() {
+    assert!(metadata(-1, "", false).is_err());
+    assert!(metadata(1, "broken", false).is_err());
+    assert!(metadata(1, "Wed, 01 Jan 2025 00:00:00 GMT", false)
+        .unwrap()
+        .last_modified()
+        .is_some());
+    assert!(metadata(1, "2025-01-01T00:00:00Z", false)
+        .unwrap()
+        .last_modified()
+        .is_some());
+    for (status, kind) in [
+        (404, ErrorKind::NotFound),
+        (403, ErrorKind::PermissionDenied),
+        (503, ErrorKind::RateLimited),
+        (429, ErrorKind::RateLimited),
+        (416, ErrorKind::RangeNotSatisfied),
+    ] {
+        let error = NativeError {
+            status,
+            ..Default::default()
+        };
+        assert_eq!(error.check(-1).unwrap_err().kind(), kind);
+    }
+}
+
+#[derive(Clone, Default)]
+struct Mock {
+    retries: Arc<AtomicUsize>,
+    partial_attempts: Arc<AtomicUsize>,
+    active: Arc<AtomicUsize>,
+    peak: Arc<AtomicUsize>,
+    heads: Arc<AtomicUsize>,
+    tokens: Arc<Mutex<Vec<String>>>,
+    ranges: Arc<Mutex<Vec<String>>>,
+    list_requests: Arc<Mutex<Vec<(String, String)>>>,
+    user_agents: Arc<Mutex<Vec<String>>>,
+}
+
+async fn mock(
+    State(state): State<Mock>,
+    method: Method,
+    uri: Uri,
+    Query(query): Query<HashMap<String, String>>,
+    headers: HeaderMap,
+) -> Response<Body> {
+    if let Some(token) = headers.get("x-oss-security-token") {
+        state
+            .tokens
+            .lock()
+            .unwrap()
+            .push(token.to_str().unwrap().to_string());
+    }
+    if let Some(ua) = headers.get("user-agent") {
+        state
+            .user_agents
+            .lock()
+            .unwrap()
+            .push(ua.to_str().unwrap().to_string());
+    }
+    let path = uri.path();
+    let status = if path.ends_with("missing") {
+        404
+    } else if path.ends_with("denied") {
+        403
+    } else if path.ends_with("throttle")
+        || (path.ends_with("retry") && state.retries.fetch_add(1, 
Ordering::SeqCst) == 0)
+    {
+        503
+    } else {
+        200
+    };
+    if status != 200 {
+        let code = match status {
+            404 => "NoSuchKey",
+            403 => "AccessDenied",
+            _ => "SlowDown",
+        };
+        return Response::builder().status(status).header("x-oss-request-id", 
"test-request")
+            
.body(Body::from(format!("<Error><Code>{code}</Code><Message>test</Message><RequestId>test-request</RequestId></Error>"))).unwrap();
+    }
+    if query.contains_key("list-type") {
+        let prefix = query.get("prefix").map(String::as_str).unwrap_or("");
+        state.list_requests.lock().unwrap().push((
+            prefix.to_string(),
+            query.get("max-keys").cloned().unwrap_or_default(),
+        ));
+        if prefix == "denied/" {
+            return Response::builder()
+                .status(StatusCode::FORBIDDEN)
+                .body(Body::from(
+                    
"<Error><Code>AccessDenied</Code><Message>test</Message></Error>",
+                ))
+                .unwrap();
+        }
+        if prefix == "missing/" {
+            return Response::new(Body::from(
+                
"<ListBucketResult><Name>bucket</Name><IsTruncated>false</IsTruncated></ListBucketResult>",
+            ));
+        }
+        let page = if prefix == "bad/" {
+            "<IsTruncated>true</IsTruncated>".to_string()
+        } else if query.contains_key("continuation-token") {
+            assert_eq!(query["continuation-token"], "page2");
+            
"<IsTruncated>false</IsTruncated><Contents><Key>objects/b</Key><Size>10</Size><LastModified>2025-01-01T00:00:00Z</LastModified></Contents>".to_string()
+        } else {
+            let common = if query.contains_key("delimiter") {
+                
"<CommonPrefixes><Prefix>objects/sub/</Prefix></CommonPrefixes>"
+            } else {
+                ""
+            };
+            
format!("<IsTruncated>true</IsTruncated><NextContinuationToken>page2</NextContinuationToken><Contents><Key>objects/a</Key><Size>10</Size><LastModified>2025-01-01T00:00:00Z</LastModified></Contents>{common}")
+        };
+        return Response::new(Body::from(format!(
+            "<ListBucketResult><Name>bucket</Name>{page}</ListBucketResult>"
+        )));
+    }
+    if method == Method::HEAD {
+        state.heads.fetch_add(1, Ordering::SeqCst);
+        return Response::builder()
+            .header("content-length", "10")
+            .header("last-modified", "Wed, 01 Jan 2025 00:00:00 GMT")
+            .body(Body::empty())
+            .unwrap();
+    }
+    if path.ends_with("slow") {
+        let active = state.active.fetch_add(1, Ordering::SeqCst) + 1;
+        state.peak.fetch_max(active, Ordering::SeqCst);
+        tokio::time::sleep(Duration::from_millis(100)).await;
+        state.active.fetch_sub(1, Ordering::SeqCst);
+    }
+    let range = headers.get("range").unwrap().to_str().unwrap().to_string();
+    if path.ends_with("partial") && state.partial_attempts.fetch_add(1, 
Ordering::SeqCst) == 0 {
+        let chunks = futures::stream::iter([
+            Ok(bytes::Bytes::from_static(b"xx")),
+            Err(std::io::Error::new(
+                std::io::ErrorKind::UnexpectedEof,
+                "test body disconnect",
+            )),
+        ]);
+        return Response::builder()
+            .status(206)
+            .header("content-range", "bytes 2-5/10")
+            .header("content-length", "4")
+            .body(Body::from_stream(chunks))
+            .unwrap();
+    }
+    state.ranges.lock().unwrap().push(range.clone());
+    let (start, end) = 
range.trim_start_matches("bytes=").split_once('-').unwrap();
+    let start: usize = start.parse().unwrap();
+    let end: usize = end.parse().unwrap();
+    if end >= 10 || start > end {
+        return Response::builder().status(416).body(Body::empty()).unwrap();
+    }
+    let content_range = if path.ends_with("wrong") {
+        "bytes 0-3/10".to_string()
+    } else {
+        format!("bytes {start}-{end}/10")
+    };
+    let body = if path.ends_with("short") {
+        "x".as_bytes()
+    } else {
+        &b"0123456789"[start..=end]
+    };
+    Response::builder()
+        .status(if path.ends_with("ignored") {
+            StatusCode::OK
+        } else {
+            StatusCode::PARTIAL_CONTENT
+        })
+        .header("content-range", content_range)
+        .body(Body::from(body))
+        .unwrap()
+}
+
+async fn fixture(
+    concurrency: usize,
+) -> (
+    Operator,
+    Mock,
+    tokio::task::JoinHandle<()>,
+    HashMap<String, String>,
+) {
+    let library = std::env::var("OSS_CPP_BRIDGE_LIBRARY")
+        .expect("Set OSS_CPP_BRIDGE_LIBRARY to the real compiled bridge");
+    let state = Mock::default();
+    let app = Router::new().fallback(mock).with_state(state.clone());
+    let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
+    let address = listener.local_addr().unwrap();
+    let server = tokio::spawn(async move { axum::serve(listener, 
app).await.unwrap() });
+    let mut options = props();
+    options.insert("fs.oss.endpoint".into(), format!("http://{address}";));
+    options.insert("fs.oss.cpp.library.path".into(), library);
+    options.insert("fs.oss.cpp.path-style".into(), "true".into());
+    options.insert(
+        "fs.oss.cpp.max.concurrent.requests".into(),
+        concurrency.to_string(),
+    );
+    options.insert("fs.oss.cpp.retry.max-attempts".into(), "2".into());
+    let config = oss_cpp_config_parse(options.clone()).unwrap();
+    (
+        oss_cpp_config_build(&config, "bucket").unwrap(),
+        state,
+        server,
+        options,
+    )
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+#[ignore = "requires the compiled OSS C++ SDK bridge; no cloud credentials"]
+async fn real_sdk_reads_errors_pagination_and_credentials() {
+    let (op, state, server, mut options) = fixture(2).await;
+    assert!(op.stat("").await.unwrap().is_dir());
+    assert!(op.stat("objects/").await.unwrap().is_dir());
+    assert!(!op.exists("missing/").await.unwrap());
+    assert_eq!(
+        op.stat("denied/").await.unwrap_err().kind(),
+        ErrorKind::PermissionDenied
+    );
+    let file_io = crate::io::FileIOBuilder::new("oss")
+        .with_props(options.clone())
+        .build()
+        .unwrap();
+    assert!(file_io.exists_dir("oss://bucket/objects").await.unwrap());
+    assert!(!file_io.exists_dir("oss://bucket/missing").await.unwrap());
+    assert!(file_io.exists_dir("oss://bucket/denied").await.is_err());
+    {
+        let list_requests = state.list_requests.lock().unwrap();
+        assert!(
+            list_requests.contains(&("objects/".to_string(), "1".to_string())),
+            "list requests: {list_requests:?}"
+        );
+    }
+    assert_eq!(op.stat("data").await.unwrap().content_length(), 10);
+    let before = state.heads.load(Ordering::SeqCst);
+    let reader = op.reader("data").await.unwrap();
+    assert_eq!(reader.read(2..6).await.unwrap().to_bytes(), &b"2345"[..]);
+    assert_eq!(
+        state.heads.load(Ordering::SeqCst),
+        before,
+        "bounded range must not HEAD"
+    );
+    assert_eq!(
+        op.read("data").await.unwrap().to_bytes(),
+        &b"0123456789"[..]
+    );
+    assert_eq!(reader.read(0..0).await.unwrap().len(), 0);
+    for key in ["short", "wrong", "ignored"] {
+        assert!(
+            op.reader(key).await.unwrap().read(2..6).await.is_err(),
+            "{key}"
+        );
+    }
+    assert!(!op.exists("missing").await.unwrap());
+    assert_eq!(
+        op.stat("denied").await.unwrap_err().kind(),
+        ErrorKind::PermissionDenied
+    );
+    assert_eq!(
+        op.reader("retry")
+            .await
+            .unwrap()
+            .read(2..6)
+            .await
+            .unwrap()
+            .to_bytes(),
+        &b"2345"[..]
+    );
+    assert_eq!(state.retries.load(Ordering::SeqCst), 2);
+    assert_eq!(
+        op.reader("partial")
+            .await
+            .unwrap()
+            .read(2..6)
+            .await
+            .unwrap()
+            .to_bytes(),
+        &b"2345"[..]
+    );
+    assert_eq!(state.partial_attempts.load(Ordering::SeqCst), 2);
+    assert_eq!(
+        reader
+            .read(BytesRange::Suffix { size: 3 })
+            .await
+            .unwrap()
+            .to_bytes(),
+        &b"789"[..]
+    );
+    assert!(reader.read(11..).await.is_err());
+    assert_eq!(
+        op.reader("throttle")
+            .await
+            .unwrap()
+            .read(2..6)
+            .await
+            .unwrap_err()
+            .kind(),
+        ErrorKind::RateLimited
+    );
+    let entries = op.list("objects/").await.unwrap();
+    assert_eq!(
+        entries.iter().map(|e| e.path()).collect::<Vec<_>>(),
+        ["objects/a", "objects/sub/", "objects/b"]
+    );
+    assert_eq!(
+        op.list_with("objects/")
+            .recursive(true)
+            .await
+            .unwrap()
+            .len(),
+        2
+    );
+    assert!(op.list("bad/").await.is_err());
+    assert!(state
+        .user_agents
+        .lock()
+        .unwrap()
+        .iter()
+        .all(|ua| ua.contains("oss-cpp")));
+    assert!(state
+        .tokens
+        .lock()
+        .unwrap()
+        .iter()
+        .all(|token| token == "test-sts"));
+    // Token refresh creates a new FileIO; do not share a client across 
credentials.
+    options.insert("fs.oss.securityToken".into(), "refreshed-sts".into());
+    let refreshed = crate::io::FileIOBuilder::new("oss")
+        .with_props(options)
+        .build()
+        .unwrap();
+    assert_eq!(
+        refreshed
+            .get_status("oss://bucket/data")
+            .await
+            .unwrap()
+            .size,
+        10
+    );
+    assert_eq!(
+        state.tokens.lock().unwrap().last().unwrap(),
+        "refreshed-sts"
+    );
+    // Existing readers retain the original client's lifetime and credentials.
+    assert_eq!(reader.read(2..6).await.unwrap().to_bytes(), &b"2345"[..]);
+    assert_eq!(state.tokens.lock().unwrap().last().unwrap(), "test-sts");
+    server.abort();
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+#[ignore = "requires the compiled OSS C++ SDK bridge; no cloud credentials"]
+async fn real_sdk_request_gate_and_cancelled_future() {
+    let (op, state, server, _) = fixture(2).await;
+    let reads = (0..8).map(|_| {
+        let op = op.clone();
+        async move {
+            op.reader("slow").await.unwrap().read(0..4).await.unwrap();
+        }
+    });
+    futures::future::join_all(reads).await;
+    assert_eq!(state.peak.load(Ordering::SeqCst), 2);
+    server.abort();
+
+    let (_, state, server, options) = fixture(1).await;
+    let file_io = crate::io::FileIOBuilder::new("oss")
+        .with_props(options)
+        .build()
+        .unwrap();
+    let reads = (0..8).map(|i| {
+        let file_io = file_io.clone();
+        async move {
+            let bucket = if i % 2 == 0 { "bucket-a" } else { "bucket-b" };
+            let path = format!("oss://{bucket}/slow");
+            let reader = 
file_io.new_input(&path).unwrap().reader().await.unwrap();
+            assert_eq!(reader.read(0..4).await.unwrap(), &b"0123"[..]);
+        }
+    });
+    futures::future::join_all(reads).await;
+    assert_eq!(
+        state.peak.load(Ordering::SeqCst),
+        1,
+        "two buckets in one FileIO must share the request gate"
+    );
+    server.abort();
+
+    let (op, state, server, _) = fixture(1).await;
+    let first = op.clone();
+    let task = tokio::spawn(async move { 
first.reader("slow").await.unwrap().read(0..4).await });
+    tokio::time::timeout(Duration::from_secs(5), async {
+        while state.active.load(Ordering::SeqCst) == 0 {
+            tokio::time::sleep(Duration::from_millis(1)).await;
+        }
+    })
+    .await
+    .unwrap();
+    task.abort();
+    op.reader("slow").await.unwrap().read(0..4).await.unwrap();
+    assert_eq!(
+        state.peak.load(Ordering::SeqCst),
+        1,
+        "cancelled caller must retain permit until IO completes"
+    );
+    server.abort();
+}
diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md
index 2d05ec11..85e6487a 100644
--- a/docs/src/getting-started.md
+++ b/docs/src/getting-started.md
@@ -45,6 +45,7 @@ Available storage features:
 | `storage-s3`     | Amazon S3        |
 | `storage-oss`    | Alibaba Cloud OSS|
 | `storage-jindo`  | Alibaba Cloud OSS through JindoSDK |
+| `storage-oss-cpp` | Alibaba Cloud OSS through C++ SDK v2 (read-only) |
 | `storage-cos`    | Tencent Cloud COS |
 | `storage-azdls`  | Azure Data Lake Storage Gen2 |
 | `storage-obs`    | Huawei Cloud OBS |
@@ -314,6 +315,12 @@ limit for the available network and JindoSDK connection 
capacity. JindoSDK state
 initialized before `fork` cannot be reused in the child; use `spawn` or 
initialize
 Jindo only after worker processes start.
 
+With `storage-oss-cpp`, set `fs.oss.impl=cpp` and
+`fs.oss.cpp.library.path` to the installed bridge library. This experimental
+backend supports stat, reads and listings, with 8 concurrent requests per
+operator by default. OpenDAL remains the default backend.
+See the [build instructions and 
options](https://github.com/apache/paimon-rust/tree/main/integrations/oss-cpp).
+
 Supported metastore types:
 
 | Metastore Type | Description                      |
diff --git a/integrations/oss-cpp/CMakeLists.txt 
b/integrations/oss-cpp/CMakeLists.txt
new file mode 100644
index 00000000..72f642e0
--- /dev/null
+++ b/integrations/oss-cpp/CMakeLists.txt
@@ -0,0 +1,25 @@
+# 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.
+
+cmake_minimum_required(VERSION 3.15)
+project(oss_cpp_bridge LANGUAGES CXX)
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+find_package(alibabacloud_oss_v2 REQUIRED CONFIG)
+add_library(oss_cpp_bridge SHARED bridge.cc)
+target_link_libraries(oss_cpp_bridge PRIVATE alibabacloud_oss_v2::oss)
+install(TARGETS oss_cpp_bridge LIBRARY DESTINATION lib)
diff --git a/integrations/oss-cpp/README.md b/integrations/oss-cpp/README.md
new file mode 100644
index 00000000..77adf236
--- /dev/null
+++ b/integrations/oss-cpp/README.md
@@ -0,0 +1,80 @@
+<!--
+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.
+-->
+
+# OSS C++ SDK backend (experimental, read-only)
+
+An opt-in, read-only FileIO backend using [Alibaba Cloud OSS C++ SDK 
v2](https://github.com/aliyun/alibabacloud-oss-cpp-sdk-v2)
+for HEAD, Range GET and paginated LIST. OpenDAL remains the default.
+
+## Build
+
+Install the SDK, then build the bridge and enable the Rust feature:
+
+```bash
+cmake -S integrations/oss-cpp -B build/oss-cpp \
+  -DCMAKE_PREFIX_PATH=/path/to/oss-sdk-install
+cmake --build build/oss-cpp
+cargo build -p paimon --features storage-oss-cpp
+```
+
+The Python binding already includes this feature, but neither the bridge nor
+the SDK is bundled in its wheel. Install both on each worker. Tested on Linux
+with SDK 0.2.0 (`c13a82038b0696f99c32f4e86b623ffcc9e62140`).
+
+## Configure
+
+Set the native table/catalog FileIO options:
+
+```text
+fs.oss.impl = cpp
+fs.oss.cpp.library.path = /absolute/path/liboss_cpp_bridge.so
+fs.oss.endpoint = https://oss-cn-shanghai-internal.aliyuncs.com
+fs.oss.region = cn-shanghai
+```
+
+Use the existing `fs.oss.accessKeyId`, `fs.oss.accessKeySecret` and optional
+`fs.oss.securityToken` credentials. REST catalog credentials still refresh;
+static FileIO credentials do not.
+
+| Option | Default |
+|---|---:|
+| `fs.oss.cpp.max.concurrent.requests` | 8 |
+| `fs.oss.cpp.connect.timeout-ms` | 10000 |
+| `fs.oss.cpp.request.timeout-ms` | 30000 |
+| `fs.oss.cpp.retry.max-attempts` | 3 |
+| `fs.oss.cpp.path-style` | false |
+
+The SDK handles retries. Timeouts are per connection/read-write, not
+end-to-end. Concurrency is bounded per FileIO, not per process; cancelled
+calls retain a slot until the SDK returns. Use spawned workers: SDK access
+after fork is rejected. Mutations and presigning are unsupported.
+Python-side image downloads are unaffected.
+
+## Test
+
+```bash
+cargo test -p paimon --features storage-oss-cpp --lib io::storage_oss_cpp
+
+OSS_CPP_BRIDGE_LIBRARY="$PWD/build/oss-cpp/liboss_cpp_bridge.so" \
+  cargo test -p paimon --features storage-oss-cpp --lib io::storage_oss_cpp \
+  -- --ignored
+```
+
+The ignored tests use the real SDK, fake credentials and a local HTTP server;
+no cloud account is needed.
diff --git a/integrations/oss-cpp/bridge.cc b/integrations/oss-cpp/bridge.cc
new file mode 100644
index 00000000..4f33a080
--- /dev/null
+++ b/integrations/oss-cpp/bridge.cc
@@ -0,0 +1,190 @@
+// 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.
+
+#include "bridge.h"
+#include <alibabacloud/oss2/ClientConfiguration.h>
+#include <alibabacloud/oss2/OSSClient.h>
+#include <alibabacloud/oss2/credentials/CredentialsProvider.h>
+#include <alibabacloud/oss2/io/ByteWriter.h>
+#include <alibabacloud/oss2/models/BucketBasic.h>
+#include <alibabacloud/oss2/models/ObjectBasic.h>
+#include <cstdio>
+#include <cstring>
+#include <limits>
+#include <memory>
+#include <string>
+
+namespace oss = alibabacloud::oss2;
+
+static int32_t failure(OssCppBridgeError *error, const char *code,
+                       int status = 0, const char *request_id = "") noexcept {
+  error->status = status;
+  std::snprintf(error->code, sizeof(error->code), "%s", code);
+  std::snprintf(error->request_id, sizeof(error->request_id), "%s", 
request_id);
+  return -1;
+}
+
+template <class F>
+static int32_t guarded(OssCppBridgeError *error, F fn) noexcept {
+  *error = {};
+  try {
+    return fn();
+  }
+  // Do not include SDK exception messages: they can contain signed requests.
+  catch (...) {
+    return failure(error, "CppException");
+  }
+}
+
+template <class E>
+static int32_t sdk_failure(OssCppBridgeError *error, const E &e) noexcept {
+  return failure(error, e.getCode().c_str(), e.getStatusCode(),
+                 e.getRequestId().c_str());
+}
+
+extern "C" {
+uint32_t oss_cpp_bridge_abi_version() { return 2; }
+
+void *oss_cpp_bridge_create(const OssCppBridgeConfig *in,
+                            OssCppBridgeError *error) {
+  void *client = nullptr;
+  guarded(error, [&]() {
+    oss::ClientConfiguration config;
+    config.endpoint = in->endpoint;
+    config.region = in->region;
+    config.credentialsProvider =
+        std::make_shared<oss::StaticCredentialsProvider>(
+            in->access_key, in->secret_key, in->token);
+    config.userAgent = in->user_agent;
+    config.connectTimeout = in->connect_timeout_ms;
+    config.readWriteTimeout = in->request_timeout_ms;
+    config.retryMaxAttempts = in->retry_attempts;
+    config.usePathStyle = in->path_style != 0;
+    client = new oss::OSSClient(config);
+    return 0;
+  });
+  return client;
+}
+
+void oss_cpp_bridge_destroy(void *client) {
+  try {
+    delete static_cast<oss::OSSClient *>(client);
+  } catch (...) {
+  }
+}
+
+int32_t oss_cpp_bridge_stat(void *client, const char *bucket, const char *key,
+                            OssCppBridgeMetadata *out,
+                            OssCppBridgeError *error) {
+  return guarded(error, [&]() {
+    auto result = static_cast<oss::OSSClient *>(client)->headObject(
+        oss::models::HeadObjectRequest().setBucket(bucket).setKey(key));
+    if (!result.has_value())
+      return sdk_failure(error, result.error());
+    const auto &value = result.value();
+    if (value.getContentLength() < 0)
+      return failure(error, "InvalidContentLength");
+    out->size = value.getContentLength();
+    std::snprintf(out->modified, sizeof(out->modified), "%s",
+                  value.getLastModified().c_str());
+    return 0;
+  });
+}
+
+int32_t oss_cpp_bridge_read(void *client, const char *bucket, const char *key,
+                            uint64_t offset, size_t length, uint8_t *buffer,
+                            OssCppBridgeError *error) {
+  return guarded(error, [&]() {
+    if (!length)
+      return 0;
+    if (offset > UINT64_MAX - (length - 1))
+      return failure(error, "InvalidRange");
+    std::shared_ptr<oss::MemoryWriter> sink;
+    oss::SinkFactory factory;
+    factory.isOneShot = false;
+    // A retry must start at the beginning of the caller's buffer.
+    factory.supplier =
+        [&](int64_t,
+            const oss::HeaderCollection &) -> std::shared_ptr<oss::ByteWriter> 
{
+      sink = std::make_shared<oss::MemoryWriter>(buffer, length);
+      return sink;
+    };
+    const auto end = offset + length - 1;
+    auto result = static_cast<oss::OSSClient *>(client)->getObject(
+        oss::models::GetObjectRequest()
+            .setBucket(bucket)
+            .setKey(key)
+            .setRange("bytes=" + std::to_string(offset) + "-" +
+                      std::to_string(end))
+            .setSinkFactory(factory));
+    if (!result.has_value())
+      return sdk_failure(error, result.error());
+    const auto &value = result.value();
+    const auto prefix =
+        "bytes " + std::to_string(offset) + "-" + std::to_string(end) + "/";
+    if (value.getStatusCode() != 206 ||
+        value.getContentRange().rfind(prefix, 0) != 0 ||
+        value.getContentLength() != static_cast<int64_t>(length) || !sink ||
+        sink->written() != length) {
+      return failure(error, "InvalidRangeResponse", value.getStatusCode());
+    }
+    return 0;
+  });
+}
+
+int32_t oss_cpp_bridge_list(void *client, const char *bucket,
+                            const char *prefix, const char *token,
+                            uint8_t recursive, uint32_t max_keys,
+                            OssCppBridgeEntry entry, void *ctx,
+                            char **next_token, OssCppBridgeError *error) {
+  *next_token = nullptr;
+  return guarded(error, [&]() {
+    auto request = oss::models::ListObjectsV2Request()
+                       .setBucket(bucket)
+                       .setPrefix(prefix)
+                       .setMaxKeys(max_keys);
+    if (!recursive)
+      request.setDelimiter("/");
+    if (*token)
+      request.setContinuationToken(token);
+    auto result = static_cast<oss::OSSClient 
*>(client)->listObjectsV2(request);
+    if (!result.has_value())
+      return sdk_failure(error, result.error());
+    const auto &value = result.value();
+    std::unique_ptr<char[]> next;
+    if (value.getIsTruncated()) {
+      const auto &s = value.getNextContinuationToken();
+      if (s.empty() || s == token)
+        return failure(error, "InvalidContinuationToken");
+      next = std::make_unique<char[]>(s.size() + 1);
+      std::memcpy(next.get(), s.c_str(), s.size() + 1);
+    }
+    for (const auto &item : value.getContents()) {
+      entry(ctx, item.key.data(), item.key.size(), item.size,
+            item.lastModified.c_str(),
+            !item.key.empty() && item.key.back() == '/');
+    }
+    for (const auto &item : value.getCommonPrefixes()) {
+      entry(ctx, item.prefix.data(), item.prefix.size(), 0, "", 1);
+    }
+    *next_token = next.release();
+    return 0;
+  });
+}
+
+void oss_cpp_bridge_free_string(char *s) { delete[] s; }
+}
diff --git a/integrations/oss-cpp/bridge.h b/integrations/oss-cpp/bridge.h
new file mode 100644
index 00000000..3cc18040
--- /dev/null
+++ b/integrations/oss-cpp/bridge.h
@@ -0,0 +1,65 @@
+// 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.
+
+#pragma once
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// ABI v2: all strings are UTF-8; inputs are borrowed for the call.
+// Clients allow concurrent calls; each call owns its error output.
+// No C++ exception may cross this boundary.
+typedef struct {
+  const char *endpoint, *region, *access_key, *secret_key, *token, *user_agent;
+  int64_t connect_timeout_ms, request_timeout_ms, retry_attempts;
+  uint8_t path_style;
+} OssCppBridgeConfig;
+
+typedef struct {
+  int32_t status;
+  char code[128];
+  char request_id[256];
+} OssCppBridgeError;
+
+typedef struct {
+  int64_t size;
+  char modified[64];
+} OssCppBridgeMetadata;
+
+typedef void (*OssCppBridgeEntry)(void *, const char *, size_t, int64_t,
+                                  const char *, uint8_t);
+
+uint32_t oss_cpp_bridge_abi_version(void);
+void *oss_cpp_bridge_create(const OssCppBridgeConfig *, OssCppBridgeError *);
+void oss_cpp_bridge_destroy(void *);
+int32_t oss_cpp_bridge_stat(void *, const char *, const char *,
+                            OssCppBridgeMetadata *, OssCppBridgeError *);
+int32_t oss_cpp_bridge_read(void *, const char *, const char *, uint64_t,
+                            size_t, uint8_t *, OssCppBridgeError *);
+// The callback borrows strings until it returns. next_token is owned by
+// the bridge and must be released with oss_cpp_bridge_free_string.
+int32_t oss_cpp_bridge_list(void *, const char *, const char *, const char *,
+                            uint8_t, uint32_t, OssCppBridgeEntry, void *,
+                            char **, OssCppBridgeError *);
+void oss_cpp_bridge_free_string(char *);
+
+#ifdef __cplusplus
+}
+#endif

Reply via email to