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])
+};