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

xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new d7eb77496 feat(bindings/cpp): init the async support of C++ binding 
(#5195)
d7eb77496 is described below

commit d7eb774966bacee2afa526e9b29a035a7c480f13
Author: Twice <[email protected]>
AuthorDate: Mon Nov 18 11:52:11 2024 +0800

    feat(bindings/cpp): init the async support of C++ binding (#5195)
---
 .github/workflows/ci_bindings_cpp.yml  |  13 +++-
 bindings/cpp/CMakeLists.txt            |  93 +++++++++++++++++++---------
 bindings/cpp/Cargo.toml                |   4 ++
 bindings/cpp/build.rs                  |  38 ++++++++++++
 bindings/cpp/include/async_defs.hpp    |  26 ++++++++
 bindings/cpp/include/opendal_async.hpp |  54 +++++++++++++++++
 bindings/cpp/src/async.rs              | 108 +++++++++++++++++++++++++++++++++
 bindings/cpp/src/lib.rs                |   2 +
 bindings/cpp/src/opendal_async.cpp     |  60 ++++++++++++++++++
 bindings/cpp/tests/async_test.cpp      |  59 ++++++++++++++++++
 10 files changed, 426 insertions(+), 31 deletions(-)

diff --git a/.github/workflows/ci_bindings_cpp.yml 
b/.github/workflows/ci_bindings_cpp.yml
index 9c1376efa..9f1fafa70 100644
--- a/.github/workflows/ci_bindings_cpp.yml
+++ b/.github/workflows/ci_bindings_cpp.yml
@@ -41,13 +41,13 @@ permissions:
 
 jobs:
   test:
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     steps:
       - uses: actions/checkout@v4
       - name: Install dependencies
         run: |
           sudo apt-get update
-          sudo apt-get install libgtest-dev ninja-build libboost-all-dev 
valgrind doxygen
+          sudo apt-get install ninja-build valgrind doxygen
 
       - name: Setup Rust toolchain
         uses: ./.github/actions/setup
@@ -69,3 +69,12 @@ jobs:
           cmake -GNinja -DOPENDAL_ENABLE_TESTING=ON ..
           ninja
           valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes 
--verbose ./opendal_cpp_test
+
+      - name: Build Cpp binding with async && Run tests
+        working-directory: "bindings/cpp"
+        run: |
+          mkdir build-async
+          cd build-async
+          cmake -GNinja -DOPENDAL_DEV=ON -DOPENDAL_ENABLE_ASYNC=ON 
-DCMAKE_CXX_COMPILER=clang++-18 ..
+          ninja
+          ./opendal_cpp_test
diff --git a/bindings/cpp/CMakeLists.txt b/bindings/cpp/CMakeLists.txt
index 0d8ce89df..b2f232c1d 100644
--- a/bindings/cpp/CMakeLists.txt
+++ b/bindings/cpp/CMakeLists.txt
@@ -21,9 +21,7 @@ project(opendal-cpp LANGUAGES CXX)
 include(FetchContent)
 set(OPENDAL_GOOGLETEST_VERSION 1.15.2 CACHE STRING "version of GoogleTest, 
'external' to fallback to find_package()")
 set(OPENDAL_BOOST_VERSION 1.86.0 CACHE STRING "version of Boost, 'external' to 
fallback to find_package()")
-
-set(CMAKE_CXX_STANDARD 17)
-set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(OPENDAL_CPPCORO_VERSION a4ef65281814b18fdd1ac5457d3e219347ec6cb8 CACHE 
STRING "version of cppcoro")
 
 if (NOT CMAKE_BUILD_TYPE)
     set(CMAKE_BUILD_TYPE Debug)
@@ -34,6 +32,18 @@ option(OPENDAL_ENABLE_DOCUMENTATION "Enable generating 
document for opendal" OFF
 option(OPENDAL_DOCS_ONLY "Only build documentation (dev only for quick ci)" 
OFF)
 option(OPENDAL_ENABLE_TESTING "Enable building test binary for opendal" OFF)
 option(OPENDAL_DEV "Enable dev mode" OFF)
+option(OPENDAL_ENABLE_ASYNC "Enable async mode (requires C++20)" OFF)
+
+if(OPENDAL_ENABLE_ASYNC)
+    set(CMAKE_CXX_STANDARD 20)
+
+    if (NOT ((CMAKE_CXX_COMPILER_ID STREQUAL "Clang") OR 
(CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")))
+        message(FATAL_ERROR "currently C++ compiler must be clang for async 
mode")
+    endif()
+else()
+    set(CMAKE_CXX_STANDARD 17)
+endif()
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
 
 if (OPENDAL_DEV)
     set(OPENDAL_ENABLE_ADDRESS_SANITIZER ON)
@@ -69,42 +79,48 @@ execute_process(COMMAND cargo locate-project --workspace 
--message-format plain
 string(REGEX REPLACE "/Cargo.toml\n$" "/target" CARGO_TARGET_DIR 
"${CARGO_TARGET_DIR}")
 set(CARGO_MANIFEST ${PROJECT_SOURCE_DIR}/Cargo.toml)
 set(RUST_SOURCE_FILE ${PROJECT_SOURCE_DIR}/src/lib.rs)
-set(RUST_BRIDGE_CPP ${CARGO_TARGET_DIR}/cxxbridge/opendal-cpp/src/lib.rs.cc)
-set(RUST_HEADER_FILE ${CARGO_TARGET_DIR}/cxxbridge/opendal-cpp/src/lib.rs.h)
+list(APPEND RUST_BRIDGE_CPP 
${CARGO_TARGET_DIR}/cxxbridge/opendal-cpp/src/lib.rs.cc)
+list(APPEND RUST_HEADER_FILE 
${CARGO_TARGET_DIR}/cxxbridge/opendal-cpp/src/lib.rs.h)
+if (OPENDAL_ENABLE_ASYNC)
+    list(APPEND RUST_BRIDGE_CPP 
${CARGO_TARGET_DIR}/cxxbridge/opendal-cpp/src/async.rs.cc)
+    list(APPEND RUST_HEADER_FILE 
${CARGO_TARGET_DIR}/cxxbridge/opendal-cpp/src/async.rs.h)
+endif()
 if (CMAKE_BUILD_TYPE STREQUAL "Debug")
     set(RUST_LIB 
${CARGO_TARGET_DIR}/debug/${CMAKE_STATIC_LIBRARY_PREFIX}opendal_cpp${CMAKE_STATIC_LIBRARY_SUFFIX})
 else()
     set(RUST_LIB 
${CARGO_TARGET_DIR}/release/${CMAKE_STATIC_LIBRARY_PREFIX}opendal_cpp${CMAKE_STATIC_LIBRARY_SUFFIX})
 endif()
 set(CPP_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/include 
${CARGO_TARGET_DIR}/cxxbridge/opendal-cpp/src)
-file(GLOB_RECURSE CPP_SOURCE_FILE src/*.cpp)
-file(GLOB_RECURSE CPP_HEADER_FILE include/*.hpp)
+list(APPEND CPP_SOURCE_FILE src/opendal.cpp)
+list(APPEND CPP_HEADER_FILE include/opendal.hpp)
+if (OPENDAL_ENABLE_ASYNC)
+    list(APPEND CPP_SOURCE_FILE src/opendal_async.cpp)
+    list(APPEND CPP_HEADER_FILE include/opendal_async.hpp)
+endif()
 
-if (CMAKE_BUILD_TYPE STREQUAL "Debug")
-    add_custom_command(
-            OUTPUT ${RUST_BRIDGE_CPP} ${RUST_LIB} ${RUST_HEADER_FILE}
-            COMMAND cargo build --manifest-path ${CARGO_MANIFEST}
-            DEPENDS ${RUST_SOURCE_FILE}
-            USES_TERMINAL
-            COMMENT "Running cargo..."
-    )
-else()
-    add_custom_command(
-            OUTPUT ${RUST_BRIDGE_CPP} ${RUST_LIB} ${RUST_HEADER_FILE}
-            COMMAND cargo build --manifest-path ${CARGO_MANIFEST} --release
-            DEPENDS ${RUST_SOURCE_FILE}
-            USES_TERMINAL
-            COMMENT "Running cargo..."
-    )
+if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
+    list(APPEND CARGO_BUILD_FLAGS "--release")
+endif()
+
+if (OPENDAL_ENABLE_ASYNC)
+    list(APPEND CARGO_BUILD_FLAGS "--features" "async")
 endif()
 
+add_custom_target(cargo_build
+        COMMAND cargo build --manifest-path ${CARGO_MANIFEST} 
${CARGO_BUILD_FLAGS}
+        BYPRODUCTS ${RUST_BRIDGE_CPP} ${RUST_LIB} ${RUST_HEADER_FILE}
+        DEPENDS ${RUST_SOURCE_FILE}
+        USES_TERMINAL
+        COMMENT "Running cargo..."
+)
+
 if(OPENDAL_BOOST_VERSION STREQUAL "external")
     find_package(Boost REQUIRED COMPONENTS date_time iostreams)
 else()
     # fetch Boost
     FetchContent_Declare(
-    Boost
-    URL 
https://github.com/boostorg/boost/releases/download/boost-${OPENDAL_BOOST_VERSION}/boost-${OPENDAL_BOOST_VERSION}-cmake.zip
+        Boost
+        URL 
https://github.com/boostorg/boost/releases/download/boost-${OPENDAL_BOOST_VERSION}/boost-${OPENDAL_BOOST_VERSION}-cmake.zip
     )
 
     set(BOOST_INCLUDE_LIBRARIES date_time iostreams system)
@@ -115,12 +131,17 @@ endif()
 add_library(opendal_cpp STATIC ${CPP_SOURCE_FILE} ${RUST_BRIDGE_CPP})
 target_sources(opendal_cpp PUBLIC ${CPP_HEADER_FILE})
 target_sources(opendal_cpp PRIVATE ${RUST_HEADER_FILE})
-target_include_directories(opendal_cpp PUBLIC ${CPP_INCLUDE_DIR} 
${Boost_INCLUDE_DIRS})
-target_link_libraries(opendal_cpp PUBLIC ${RUST_LIB})
-target_link_libraries(opendal_cpp PRIVATE ${CMAKE_DL_LIBS} Boost::date_time)
+target_include_directories(opendal_cpp PUBLIC ${CPP_INCLUDE_DIR})
+if (OPENDAL_ENABLE_ASYNC)
+    target_include_directories(opendal_cpp PUBLIC 
${CARGO_TARGET_DIR}/cxxbridge)
+    target_compile_options(opendal_cpp PUBLIC -include 
${PROJECT_SOURCE_DIR}/include/async_defs.hpp)
+endif()
+target_link_libraries(opendal_cpp PUBLIC ${RUST_LIB} Boost::date_time 
Boost::iostreams)
+target_link_libraries(opendal_cpp PRIVATE ${CMAKE_DL_LIBS})
 set_target_properties(opendal_cpp
         PROPERTIES ADDITIONAL_CLEAN_FILES ${CARGO_TARGET_DIR}
 )
+add_dependencies(opendal_cpp cargo_build)
 
 if (OPENDAL_ENABLE_ADDRESS_SANITIZER)
     target_compile_options(opendal_cpp PRIVATE 
-fsanitize=leak,address,undefined -fno-omit-frame-pointer -fno-common -O1)
@@ -156,11 +177,25 @@ if (OPENDAL_ENABLE_TESTING)
         FetchContent_MakeAvailable(googletest)
     endif()
 
-    file(GLOB_RECURSE TEST_SOURCE_FILE tests/*.cpp)
+    if (OPENDAL_ENABLE_ASYNC)
+        FetchContent_Declare(
+            cppcoro
+            URL 
https://github.com/andreasbuhr/cppcoro/archive/${OPENDAL_CPPCORO_VERSION}.zip
+        )
+        FetchContent_MakeAvailable(cppcoro)
+    endif()
+
+    list(APPEND TEST_SOURCE_FILE tests/basic_test.cpp)
+    if (OPENDAL_ENABLE_ASYNC)
+        list(APPEND TEST_SOURCE_FILE tests/async_test.cpp)
+    endif()
     add_executable(opendal_cpp_test ${TEST_SOURCE_FILE})
     target_include_directories(opendal_cpp_test PUBLIC ${CPP_INCLUDE_DIR} 
${GTEST_INCLUDE_DIRS})
     target_link_libraries(opendal_cpp_test ${GTEST_LDFLAGS} GTest::gtest_main 
opendal_cpp)
     target_compile_options(opendal_cpp_test PRIVATE ${GTEST_CFLAGS})
+    if (OPENDAL_ENABLE_ASYNC)
+        target_link_libraries(opendal_cpp_test cppcoro)
+    endif()
 
     # enable address sanitizers
     if (OPENDAL_ENABLE_ADDRESS_SANITIZER)
diff --git a/bindings/cpp/Cargo.toml b/bindings/cpp/Cargo.toml
index 4f711b7e9..2d82dc8c2 100644
--- a/bindings/cpp/Cargo.toml
+++ b/bindings/cpp/Cargo.toml
@@ -34,6 +34,7 @@ crate-type = ["staticlib"]
 anyhow = "1.0"
 chrono = "0.4"
 cxx = "1.0"
+cxx-async = { version = "0.1.2", optional = true }
 # this crate won't be published, we always use the local version
 opendal = { version = ">=0", path = "../../core", features = [
   # These are default features before v0.46. TODO: change to optional features
@@ -56,3 +57,6 @@ opendal = { version = ">=0", path = "../../core", features = [
 
 [build-dependencies]
 cxx-build = "1.0"
+
+[features]
+async = ["cxx-async", "cxx/c++20"]
diff --git a/bindings/cpp/build.rs b/bindings/cpp/build.rs
index 7d5d10f7b..168102b0a 100644
--- a/bindings/cpp/build.rs
+++ b/bindings/cpp/build.rs
@@ -15,8 +15,46 @@
 // specific language governing permissions and limitations
 // under the License.
 
+#[cfg(feature = "async")]
+mod build_async {
+    use std::{
+        env::var,
+        io,
+        path::{Path, PathBuf},
+    };
+
+    fn copy_force<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> 
io::Result<()> {
+        if dst.as_ref().exists() {
+            std::fs::remove_file(&dst)?;
+        }
+
+        std::fs::copy(src, dst)?;
+        Ok(())
+    }
+
+    pub fn symlink_async_includes() {
+        let async_inc = var("DEP_CXX_ASYNC_INCLUDE").unwrap();
+        let src_dir = PathBuf::from(async_inc).join("rust");
+
+        let prj_dir = var("CARGO_MANIFEST_DIR").unwrap();
+        let dst_dir = PathBuf::from(prj_dir)
+            .join("target")
+            .join("cxxbridge")
+            .join("rust");
+
+        copy_force(src_dir.join("cxx_async.h"), 
dst_dir.join("cxx_async.h")).unwrap();
+    }
+}
+
 fn main() {
     let _ = cxx_build::bridge("src/lib.rs");
+    #[cfg(feature = "async")]
+    {
+        let _ = cxx_build::bridge("src/async.rs");
+        build_async::symlink_async_includes();
+    }
 
     println!("cargo:rerun-if-changed=src/lib.rs");
+    #[cfg(feature = "async")]
+    println!("cargo:rerun-if-changed=src/async.rs");
 }
diff --git a/bindings/cpp/include/async_defs.hpp 
b/bindings/cpp/include/async_defs.hpp
new file mode 100644
index 000000000..c8a831d0c
--- /dev/null
+++ b/bindings/cpp/include/async_defs.hpp
@@ -0,0 +1,26 @@
+/*
+ * 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 "rust/cxx.h"
+#include "rust/cxx_async.h"
+
+CXXASYNC_DEFINE_FUTURE(rust::Vec<uint8_t>, opendal, ffi, async, 
RustFutureRead);
+CXXASYNC_DEFINE_FUTURE(void, opendal, ffi, async, RustFutureWrite);
diff --git a/bindings/cpp/include/opendal_async.hpp 
b/bindings/cpp/include/opendal_async.hpp
new file mode 100644
index 000000000..45524df5b
--- /dev/null
+++ b/bindings/cpp/include/opendal_async.hpp
@@ -0,0 +1,54 @@
+/*
+ * 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 <optional>
+#include <span>
+
+#include "async.rs.h"
+#include "async_defs.hpp"
+
+namespace opendal::async {
+
+class Operator {
+ public:
+  Operator(std::string_view scheme,
+           const std::unordered_map<std::string, std::string> &config = {});
+
+  // Disable copy and assign
+  Operator(const Operator &) = delete;
+  Operator &operator=(const Operator &) = delete;
+
+  // Enable move
+  Operator(Operator &&) = default;
+  Operator &operator=(Operator &&) = default;
+  ~Operator() = default;
+
+  using ReadFuture = opendal::ffi::async::RustFutureRead;
+  ReadFuture read(std::string_view path);
+
+  using WriteFuture = opendal::ffi::async::RustFutureWrite;
+  WriteFuture write(std::string_view path, std::span<uint8_t> data);
+
+ private:
+  rust::Box<opendal::ffi::async::Operator> operator_;
+};
+
+}  // namespace opendal::async
diff --git a/bindings/cpp/src/async.rs b/bindings/cpp/src/async.rs
new file mode 100644
index 000000000..595006f26
--- /dev/null
+++ b/bindings/cpp/src/async.rs
@@ -0,0 +1,108 @@
+// 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 anyhow::Result;
+use cxx_async::CxxAsyncException;
+use opendal as od;
+use std::collections::HashMap;
+use std::future::Future;
+use std::ops::Deref;
+use std::str::FromStr;
+
+#[cxx::bridge(namespace = "opendal::ffi::async")]
+mod ffi {
+    struct HashMapValue {
+        key: String,
+        value: String,
+    }
+
+    // here we have to use raw pointers since:
+    // 1. cxx-async futures requires 'static lifetime (and it's hard to change 
for now)
+    // 2. cxx SharedPtr cannot accept Rust types as type parameters for now
+    pub struct OperatorPtr {
+        op: *const Operator,
+    }
+
+    extern "Rust" {
+        type Operator;
+
+        fn new_operator(scheme: &str, configs: Vec<HashMapValue>) -> 
Result<Box<Operator>>;
+        unsafe fn operator_read(op: OperatorPtr, path: String) -> 
RustFutureRead;
+        unsafe fn operator_write(op: OperatorPtr, path: String, bs: Vec<u8>) 
-> RustFutureWrite;
+    }
+
+    extern "C++" {
+        type RustFutureRead = super::RustFutureRead;
+        type RustFutureWrite = super::RustFutureWrite;
+    }
+}
+
+#[cxx_async::bridge(namespace = opendal::ffi::async)]
+unsafe impl Future for RustFutureRead {
+    type Output = Vec<u8>;
+}
+
+#[cxx_async::bridge(namespace = opendal::ffi::async)]
+unsafe impl Future for RustFutureWrite {
+    type Output = ();
+}
+
+pub struct Operator(od::Operator);
+
+fn new_operator(scheme: &str, configs: Vec<ffi::HashMapValue>) -> 
Result<Box<Operator>> {
+    let scheme = od::Scheme::from_str(scheme)?;
+
+    let map: HashMap<String, String> = configs
+        .into_iter()
+        .map(|value| (value.key, value.value))
+        .collect();
+
+    let op = Box::new(Operator(od::Operator::via_iter(scheme, map)?));
+
+    Ok(op)
+}
+
+impl Deref for ffi::OperatorPtr {
+    type Target = Operator;
+
+    fn deref(&self) -> &Self::Target {
+        unsafe { &*self.op }
+    }
+}
+
+unsafe impl Send for ffi::OperatorPtr {}
+
+unsafe fn operator_read(op: ffi::OperatorPtr, path: String) -> RustFutureRead {
+    RustFutureRead::fallible(async move {
+        Ok((*op)
+            .0
+            .read(&path)
+            .await
+            .map_err(|e| 
CxxAsyncException::new(e.to_string().into_boxed_str()))?
+            .to_vec())
+    })
+}
+
+unsafe fn operator_write(op: ffi::OperatorPtr, path: String, bs: Vec<u8>) -> 
RustFutureWrite {
+    RustFutureWrite::fallible(async move {
+        Ok((*op)
+            .0
+            .write(&path, bs)
+            .await
+            .map_err(|e| 
CxxAsyncException::new(e.to_string().into_boxed_str()))?)
+    })
+}
diff --git a/bindings/cpp/src/lib.rs b/bindings/cpp/src/lib.rs
index a37807c07..957134501 100644
--- a/bindings/cpp/src/lib.rs
+++ b/bindings/cpp/src/lib.rs
@@ -15,6 +15,8 @@
 // specific language governing permissions and limitations
 // under the License.
 
+#[cfg(feature = "async")]
+mod r#async;
 mod lister;
 mod reader;
 mod types;
diff --git a/bindings/cpp/src/opendal_async.cpp 
b/bindings/cpp/src/opendal_async.cpp
new file mode 100644
index 000000000..6ec2dccad
--- /dev/null
+++ b/bindings/cpp/src/opendal_async.cpp
@@ -0,0 +1,60 @@
+/*
+ * 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 "opendal_async.hpp"
+
+#include <iterator>
+
+#include "async.rs.h"
+#include "async_defs.hpp"
+
+#define RUST_STR(s) rust::Str(s.data(), s.size())
+#define RUST_STRING(s) rust::String(s.data(), s.size())
+
+using namespace opendal::async;
+
+static rust::Box<opendal::ffi::async::Operator> new_operator(
+    std::string_view scheme,
+    const std::unordered_map<std::string, std::string> &config) {
+  auto rust_map = rust::Vec<opendal::ffi::async::HashMapValue>();
+  rust_map.reserve(config.size());
+  for (auto &[k, v] : config) {
+    rust_map.push_back({RUST_STRING(k), RUST_STRING(v)});
+  }
+
+  return opendal::ffi::async::new_operator(RUST_STR(scheme), rust_map);
+}
+
+Operator::Operator(std::string_view scheme,
+                   const std::unordered_map<std::string, std::string> &config)
+    : operator_(new_operator(scheme, config)) {}
+
+Operator::ReadFuture Operator::read(std::string_view path) {
+  return opendal::ffi::async::operator_read(
+      opendal::ffi::async::OperatorPtr{&*operator_}, RUST_STRING(path));
+}
+
+Operator::WriteFuture Operator::write(std::string_view path,
+                                      std::span<uint8_t> data) {
+  rust::Vec<uint8_t> vec;
+  std::copy(data.begin(), data.end(), std::back_inserter(vec));
+
+  return opendal::ffi::async::operator_write(
+      opendal::ffi::async::OperatorPtr{&*operator_}, RUST_STRING(path), vec);
+}
diff --git a/bindings/cpp/tests/async_test.cpp 
b/bindings/cpp/tests/async_test.cpp
new file mode 100644
index 000000000..dc25c2068
--- /dev/null
+++ b/bindings/cpp/tests/async_test.cpp
@@ -0,0 +1,59 @@
+/*
+ * 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 <random>
+
+#include "cppcoro/sync_wait.hpp"
+#include "cppcoro/task.hpp"
+#include "gtest/gtest.h"
+#include "opendal_async.hpp"
+
+class AsyncOpendalTest : public ::testing::Test {
+ protected:
+  std::optional<opendal::async::Operator> op;
+
+  std::string scheme;
+  std::unordered_map<std::string, std::string> config;
+
+  // random number generator
+  std::mt19937 rng;
+
+  void SetUp() override {
+    scheme = "memory";
+    rng.seed(time(nullptr));
+
+    op = opendal::async::Operator(scheme, config);
+  }
+};
+
+TEST_F(AsyncOpendalTest, BasicTest) {
+  auto path = "test_path";
+  std::vector<uint8_t> data{1, 2, 3, 4, 5};
+  cppcoro::sync_wait(op->write(path, data));
+  auto res = cppcoro::sync_wait(op->read(path));
+  for (size_t i = 0; i < data.size(); ++i) EXPECT_EQ(data[i], res[i]);
+
+  path = "test_path2";
+  cppcoro::sync_wait([&]() -> cppcoro::task<void> {
+    co_await op->write(path, data);
+    auto res = co_await op->read(path);
+    for (size_t i = 0; i < data.size(); ++i) EXPECT_EQ(data[i], res[i]);
+    co_return;
+  }());
+}

Reply via email to