You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by yu...@apache.org on 2026/05/24 15:27:40 UTC

(opendal) 01/02: feat(bindings/c): support layers

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

yuchanns pushed a commit to branch feat/bindings-go-layer
in repository https://gitbox.apache.org/repos/asf/opendal.git

commit 175697e9f6169531129c4d5e70ff4236de49525b
Author: Hanchin Hsieh <me...@yuchanns.xyz>
AuthorDate: Sun May 24 23:27:16 2026 +0800

    feat(bindings/c): support layers
---
 bindings/c/include/opendal.h           |  54 +++++++++++
 bindings/c/src/layer.rs                | 166 +++++++++++++++++++++++++++++++++
 bindings/c/src/lib.rs                  |   3 +
 bindings/c/src/operator.rs             |  90 ++++++++++++++----
 bindings/c/tests/Makefile              |   3 +-
 bindings/c/tests/test_runner.cpp       |   2 +
 bindings/c/tests/test_suites_layer.cpp | 109 ++++++++++++++++++++++
 7 files changed, 408 insertions(+), 19 deletions(-)

diff --git a/bindings/c/include/opendal.h b/bindings/c/include/opendal.h
index b845ab310..0b2c14269 100644
--- a/bindings/c/include/opendal.h
+++ b/bindings/c/include/opendal.h
@@ -188,6 +188,19 @@ typedef struct opendal_lister {
   void *inner;
 } opendal_lister;
 
+/**
+ * \brief The layers to apply when initializing an opendal_operator.
+ *
+ * \note This is also a heap-allocated struct, please free it after you use it.
+ */
+typedef struct opendal_operator_layers {
+  /**
+   * The pointer to the Vec<OperatorLayer> in the Rust code.
+   * Only touch this on judging whether it is NULL.
+   */
+  void *inner;
+} opendal_operator_layers;
+
 /**
  * \brief Carries all metadata associated with a **path**.
  *
@@ -698,6 +711,33 @@ struct opendal_result_lister_next opendal_lister_next(struct opendal_lister *sel
  */
 void opendal_lister_free(struct opendal_lister *ptr);
 
+/**
+ * \brief Construct a heap-allocated opendal_operator_layers.
+ */
+struct opendal_operator_layers *opendal_operator_layers_new(void);
+
+/**
+ * \brief Add a retry layer.
+ */
+void opendal_operator_layers_add_retry(struct opendal_operator_layers *self,
+                                       uint8_t jitter,
+                                       float factor,
+                                       uint64_t min_delay_ns,
+                                       uint64_t max_delay_ns,
+                                       uint64_t max_times);
+
+/**
+ * \brief Add a timeout layer.
+ */
+void opendal_operator_layers_add_timeout(struct opendal_operator_layers *self,
+                                         uint64_t timeout_ns,
+                                         uint64_t io_timeout_ns);
+
+/**
+ * \brief Free the allocated memory used by opendal_operator_layers.
+ */
+void opendal_operator_layers_free(struct opendal_operator_layers *ptr);
+
 /**
  * \brief Free the heap-allocated metadata used by opendal_metadata
  */
@@ -829,6 +869,20 @@ void opendal_operator_free(const struct opendal_operator *ptr);
 struct opendal_result_operator_new opendal_operator_new(const char *scheme,
                                                         const struct opendal_operator_options *options);
 
+/**
+ * \brief Construct an operator based on scheme, options, and explicit layers.
+ *
+ * Unlike opendal_operator_new, this function will not add any default layer.
+ * Layers will be applied exactly as they were added to opendal_operator_layers.
+ *
+ * # Safety
+ *
+ * The only unsafe case is passing an invalid c string pointer to the scheme argument.
+ */
+struct opendal_result_operator_new opendal_operator_new_with_layers(const char *scheme,
+                                                                    const struct opendal_operator_options *options,
+                                                                    const struct opendal_operator_layers *layers);
+
 /**
  * \brief Blocking write raw bytes to `path`.
  *
diff --git a/bindings/c/src/layer.rs b/bindings/c/src/layer.rs
new file mode 100644
index 000000000..6cc17dd64
--- /dev/null
+++ b/bindings/c/src/layer.rs
@@ -0,0 +1,166 @@
+// 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 std::time::Duration;
+
+use ::opendal as core;
+
+enum OperatorLayer {
+    Retry {
+        jitter: bool,
+        factor: f32,
+        min_delay: Duration,
+        max_delay: Duration,
+        max_times: u64,
+    },
+    Timeout {
+        timeout: Duration,
+        io_timeout: Duration,
+    },
+}
+
+impl OperatorLayer {
+    fn apply(&self, op: core::Operator) -> core::Operator {
+        match self {
+            Self::Retry {
+                jitter,
+                factor,
+                min_delay,
+                max_delay,
+                max_times,
+            } => {
+                let mut layer = core::layers::RetryLayer::new()
+                    .with_factor(*factor)
+                    .with_min_delay(*min_delay)
+                    .with_max_delay(*max_delay)
+                    .with_max_times(*max_times as usize);
+                if *jitter {
+                    layer = layer.with_jitter();
+                }
+                op.layer(layer)
+            }
+            Self::Timeout {
+                timeout,
+                io_timeout,
+            } => op.layer(
+                core::layers::TimeoutLayer::new()
+                    .with_timeout(*timeout)
+                    .with_io_timeout(*io_timeout),
+            ),
+        }
+    }
+}
+
+/// \brief The layers to apply when initializing an opendal_operator.
+///
+/// \note This is also a heap-allocated struct, please free it after you use it.
+#[repr(C)]
+pub struct opendal_operator_layers {
+    /// The pointer to the Vec<OperatorLayer> in the Rust code.
+    /// Only touch this on judging whether it is NULL.
+    inner: *mut c_void,
+}
+
+impl opendal_operator_layers {
+    pub(crate) fn apply(&self, mut op: core::Operator) -> core::Operator {
+        for layer in self.deref() {
+            op = layer.apply(op);
+        }
+        op
+    }
+
+    fn deref(&self) -> &[OperatorLayer] {
+        // Safety: the inner should never be null once constructed.
+        unsafe { &*(self.inner as *mut Vec<OperatorLayer>) }
+    }
+
+    fn deref_mut(&mut self) -> &mut Vec<OperatorLayer> {
+        // Safety: the inner should never be null once constructed.
+        unsafe { &mut *(self.inner as *mut Vec<OperatorLayer>) }
+    }
+
+    /// \brief Construct a heap-allocated opendal_operator_layers.
+    #[no_mangle]
+    pub extern "C" fn opendal_operator_layers_new() -> *mut Self {
+        let layers: Vec<OperatorLayer> = Vec::new();
+        let layers = Self {
+            inner: Box::into_raw(Box::new(layers)) as _,
+        };
+        Box::into_raw(Box::new(layers))
+    }
+
+    /// \brief Add a retry layer.
+    pub fn add_retry(
+        &mut self,
+        jitter: u8,
+        factor: f32,
+        min_delay_ns: u64,
+        max_delay_ns: u64,
+        max_times: u64,
+    ) {
+        self.deref_mut().push(OperatorLayer::Retry {
+            jitter: jitter != 0,
+            factor,
+            min_delay: Duration::from_nanos(min_delay_ns),
+            max_delay: Duration::from_nanos(max_delay_ns),
+            max_times,
+        });
+    }
+
+    /// \brief Add a retry layer.
+    #[no_mangle]
+    pub unsafe extern "C" fn opendal_operator_layers_add_retry(
+        &mut self,
+        jitter: u8,
+        factor: f32,
+        min_delay_ns: u64,
+        max_delay_ns: u64,
+        max_times: u64,
+    ) {
+        self.add_retry(jitter, factor, min_delay_ns, max_delay_ns, max_times);
+    }
+
+    /// \brief Add a timeout layer.
+    pub fn add_timeout(&mut self, timeout_ns: u64, io_timeout_ns: u64) {
+        self.deref_mut().push(OperatorLayer::Timeout {
+            timeout: Duration::from_nanos(timeout_ns),
+            io_timeout: Duration::from_nanos(io_timeout_ns),
+        });
+    }
+
+    /// \brief Add a timeout layer.
+    #[no_mangle]
+    pub unsafe extern "C" fn opendal_operator_layers_add_timeout(
+        &mut self,
+        timeout_ns: u64,
+        io_timeout_ns: u64,
+    ) {
+        self.add_timeout(timeout_ns, io_timeout_ns);
+    }
+
+    /// \brief Free the allocated memory used by opendal_operator_layers.
+    #[no_mangle]
+    pub unsafe extern "C" fn opendal_operator_layers_free(ptr: *mut opendal_operator_layers) {
+        unsafe {
+            if !ptr.is_null() {
+                drop(Box::from_raw((*ptr).inner as *mut Vec<OperatorLayer>));
+                drop(Box::from_raw(ptr));
+            }
+        }
+    }
+}
diff --git a/bindings/c/src/lib.rs b/bindings/c/src/lib.rs
index 754df7b35..ede881e03 100644
--- a/bindings/c/src/lib.rs
+++ b/bindings/c/src/lib.rs
@@ -39,6 +39,9 @@ pub use error::opendal_error;
 mod lister;
 pub use lister::opendal_lister;
 
+mod layer;
+pub use layer::opendal_operator_layers;
+
 mod metadata;
 pub use metadata::opendal_metadata;
 
diff --git a/bindings/c/src/operator.rs b/bindings/c/src/operator.rs
index 428ce4439..bb6de2f5a 100644
--- a/bindings/c/src/operator.rs
+++ b/bindings/c/src/operator.rs
@@ -92,6 +92,25 @@ fn build_operator(
 
     let op = core::Operator::via_iter(schema, map)?.layer(core::layers::RetryLayer::new());
 
+    build_blocking_operator(op)
+}
+
+fn build_operator_with_layers(
+    schema: &str,
+    map: HashMap<String, String>,
+    layers: *const opendal_operator_layers,
+) -> core::Result<core::blocking::Operator> {
+    core::init_default_registry();
+
+    let mut op = core::Operator::via_iter(schema, map)?;
+    if !layers.is_null() {
+        op = unsafe { (*layers).apply(op) };
+    }
+
+    build_blocking_operator(op)
+}
+
+fn build_blocking_operator(op: core::Operator) -> core::Result<core::blocking::Operator> {
     let runtime =
         tokio::runtime::Handle::try_current().unwrap_or_else(|_| RUNTIME.handle().clone());
     let _guard = runtime.enter();
@@ -99,6 +118,33 @@ fn build_operator(
     Ok(op)
 }
 
+fn parse_operator_options(options: *const opendal_operator_options) -> HashMap<String, String> {
+    let mut map = HashMap::<String, String>::default();
+    if !options.is_null() {
+        unsafe {
+            for (k, v) in (*options).deref() {
+                map.insert(k.to_string(), v.to_string());
+            }
+        }
+    }
+    map
+}
+
+fn new_operator_result(op: core::Result<core::blocking::Operator>) -> opendal_result_operator_new {
+    match op {
+        Ok(op) => opendal_result_operator_new {
+            op: Box::into_raw(Box::new(opendal_operator {
+                inner: Box::into_raw(Box::new(op)) as _,
+            })),
+            error: std::ptr::null_mut(),
+        },
+        Err(e) => opendal_result_operator_new {
+            op: std::ptr::null_mut(),
+            error: opendal_error::new(e),
+        },
+    }
+}
+
 /// \brief Construct an operator based on `scheme` and `options`
 ///
 /// Uses an array of key-value pairs to initialize the operator based on provided `scheme`
@@ -146,25 +192,33 @@ pub unsafe extern "C" fn opendal_operator_new(
         .to_str()
         .expect("malformed scheme");
 
-    let mut map = HashMap::<String, String>::default();
-    if !options.is_null() {
-        for (k, v) in (*options).deref() {
-            map.insert(k.to_string(), v.to_string());
-        }
-    }
+    let map = parse_operator_options(options);
 
-    match build_operator(scheme, map) {
-        Ok(op) => opendal_result_operator_new {
-            op: Box::into_raw(Box::new(opendal_operator {
-                inner: Box::into_raw(Box::new(op)) as _,
-            })),
-            error: std::ptr::null_mut(),
-        },
-        Err(e) => opendal_result_operator_new {
-            op: std::ptr::null_mut(),
-            error: opendal_error::new(e),
-        },
-    }
+    new_operator_result(build_operator(scheme, map))
+}
+
+/// \brief Construct an operator based on scheme, options, and explicit layers.
+///
+/// Unlike opendal_operator_new, this function will not add any default layer.
+/// Layers will be applied exactly as they were added to opendal_operator_layers.
+///
+/// # Safety
+///
+/// The only unsafe case is passing an invalid c string pointer to the scheme argument.
+#[no_mangle]
+pub unsafe extern "C" fn opendal_operator_new_with_layers(
+    scheme: *const c_char,
+    options: *const opendal_operator_options,
+    layers: *const opendal_operator_layers,
+) -> opendal_result_operator_new {
+    assert!(!scheme.is_null());
+    let scheme = std::ffi::CStr::from_ptr(scheme)
+        .to_str()
+        .expect("malformed scheme");
+
+    let map = parse_operator_options(options);
+
+    new_operator_result(build_operator_with_layers(scheme, map, layers))
 }
 
 /// \brief Blocking write raw bytes to `path`.
diff --git a/bindings/c/tests/Makefile b/bindings/c/tests/Makefile
index 630070022..867a5287a 100644
--- a/bindings/c/tests/Makefile
+++ b/bindings/c/tests/Makefile
@@ -26,7 +26,7 @@ endif
 
 # Source files
 FRAMEWORK_SOURCES = test_framework.cpp
-SUITE_SOURCES = test_suites_basic.cpp test_suites_list.cpp test_suites_reader_writer.cpp test_suites_presign.cpp
+SUITE_SOURCES = test_suites_basic.cpp test_suites_list.cpp test_suites_reader_writer.cpp test_suites_presign.cpp test_suites_layer.cpp
 RUNNER_SOURCES = test_runner.cpp
 ALL_SOURCES = $(FRAMEWORK_SOURCES) $(SUITE_SOURCES) $(RUNNER_SOURCES)
 
@@ -105,4 +105,5 @@ test_framework.o: test_framework.h
 test_suites_basic.o: test_framework.h
 test_suites_list.o: test_framework.h
 test_suites_reader_writer.o: test_framework.h
+test_suites_layer.o: test_framework.h
 test_runner.o: test_framework.h
diff --git a/bindings/c/tests/test_runner.cpp b/bindings/c/tests/test_runner.cpp
index cf1bf2397..63a6c1ff1 100644
--- a/bindings/c/tests/test_runner.cpp
+++ b/bindings/c/tests/test_runner.cpp
@@ -24,6 +24,7 @@
 // External test suite declarations
 extern opendal_test_suite basic_suite;
 extern opendal_test_suite list_suite;
+extern opendal_test_suite layer_suite;
 extern opendal_test_suite presign_suite;
 extern opendal_test_suite reader_writer_suite;
 
@@ -31,6 +32,7 @@ extern opendal_test_suite reader_writer_suite;
 static opendal_test_suite* all_suites[] = {
     &basic_suite,
     &list_suite,
+    &layer_suite,
     &presign_suite,
     &reader_writer_suite,
 };
diff --git a/bindings/c/tests/test_suites_layer.cpp b/bindings/c/tests/test_suites_layer.cpp
new file mode 100644
index 000000000..774170f74
--- /dev/null
+++ b/bindings/c/tests/test_suites_layer.cpp
@@ -0,0 +1,109 @@
+/*
+ * 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 "test_framework.h"
+
+void test_operator_with_timeout_layer(opendal_test_context* ctx)
+{
+    opendal_operator_layers* layers = opendal_operator_layers_new();
+    OPENDAL_ASSERT_NOT_NULL(layers, "Should create operator layers");
+
+    opendal_operator_layers_add_timeout(layers, 60ULL * 1000 * 1000 * 1000, 10ULL * 1000 * 1000 * 1000);
+
+    opendal_result_operator_new result = opendal_operator_new_with_layers(
+        ctx->config->scheme, ctx->config->options, layers);
+    opendal_operator_layers_free(layers);
+
+    OPENDAL_ASSERT_NO_ERROR(result.error, "Operator with timeout layer should be created");
+    OPENDAL_ASSERT_NOT_NULL(result.op, "Operator with timeout layer should not be null");
+
+    opendal_operator_info* info = opendal_operator_info_new(result.op);
+    OPENDAL_ASSERT_NOT_NULL(info, "Layered operator info should not be null");
+    opendal_operator_info_free(info);
+
+    const char* path = "test_timeout_layer.txt";
+    const char* content = "Hello, OpenDAL timeout layer!";
+    opendal_bytes data;
+    data.data = (uint8_t*)content;
+    data.len = strlen(content);
+    data.capacity = strlen(content);
+
+    opendal_error* error = opendal_operator_write(result.op, path, &data);
+    OPENDAL_ASSERT_NO_ERROR(error, "Write with timeout layer should succeed");
+
+    opendal_result_read read_result = opendal_operator_read(result.op, path);
+    OPENDAL_ASSERT_NO_ERROR(read_result.error, "Read with timeout layer should succeed");
+    OPENDAL_ASSERT_EQ(strlen(content), read_result.data.len, "Read data length should match");
+    OPENDAL_ASSERT(memcmp(content, read_result.data.data, read_result.data.len) == 0,
+        "Read content should match");
+
+    opendal_bytes_free(&read_result.data);
+    opendal_operator_delete(result.op, path);
+    opendal_operator_free(result.op);
+}
+
+void test_operator_with_retry_layer(opendal_test_context* ctx)
+{
+    opendal_operator_layers* layers = opendal_operator_layers_new();
+    OPENDAL_ASSERT_NOT_NULL(layers, "Should create operator layers");
+
+    opendal_operator_layers_add_retry(layers, 1, 2.0f, 1000ULL * 1000 * 1000, 60ULL * 1000 * 1000 * 1000, 3);
+
+    opendal_result_operator_new result = opendal_operator_new_with_layers(
+        ctx->config->scheme, ctx->config->options, layers);
+    opendal_operator_layers_free(layers);
+
+    OPENDAL_ASSERT_NO_ERROR(result.error, "Operator with retry layer should be created");
+    OPENDAL_ASSERT_NOT_NULL(result.op, "Operator with retry layer should not be null");
+
+    opendal_operator_info* info = opendal_operator_info_new(result.op);
+    OPENDAL_ASSERT_NOT_NULL(info, "Layered operator info should not be null");
+    opendal_operator_info_free(info);
+
+    const char* path = "test_retry_layer.txt";
+    const char* content = "Hello, OpenDAL retry layer!";
+    opendal_bytes data;
+    data.data = (uint8_t*)content;
+    data.len = strlen(content);
+    data.capacity = strlen(content);
+
+    opendal_error* error = opendal_operator_write(result.op, path, &data);
+    OPENDAL_ASSERT_NO_ERROR(error, "Write with retry layer should succeed");
+
+    opendal_result_read read_result = opendal_operator_read(result.op, path);
+    OPENDAL_ASSERT_NO_ERROR(read_result.error, "Read with retry layer should succeed");
+    OPENDAL_ASSERT_EQ(strlen(content), read_result.data.len, "Read data length should match");
+    OPENDAL_ASSERT(memcmp(content, read_result.data.data, read_result.data.len) == 0,
+        "Read content should match");
+
+    opendal_bytes_free(&read_result.data);
+    opendal_operator_delete(result.op, path);
+    opendal_operator_free(result.op);
+}
+
+opendal_test_case layer_tests[] = {
+    { "operator_with_retry_layer", test_operator_with_retry_layer, make_capability_read_write() },
+    { "operator_with_timeout_layer", test_operator_with_timeout_layer, make_capability_read_write() },
+};
+
+opendal_test_suite layer_suite = {
+    "Layer",
+    layer_tests,
+    sizeof(layer_tests) / sizeof(layer_tests[0])
+};