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 5e074cc49b feat(bindings/d): add D bindings support (#5181)
5e074cc49b is described below
commit 5e074cc49bc7662c790d88fb9be31e7c67899d64
Author: Matheus C. França <[email protected]>
AuthorDate: Tue Oct 22 10:05:41 2024 -0300
feat(bindings/d): add D bindings support (#5181)
D bindings
* `std.paralellism` support added (write/read/list)
* CI/CD test
* rename `is_exists` - ref.: https://github.com/apache/opendal/pull/5199
Signed-off-by: Matheus C. França <[email protected]>
---
.devcontainer/post_create.sh | 3 +
.gitattributes | 1 +
.github/workflows/ci_bindings_d.yml | 73 +++++++++++
.typos.toml | 2 +
README.md | 2 +
bindings/README.md | 1 +
bindings/d/.gitignore | 20 +++
bindings/d/CONTRIBUTING.md | 58 +++++++++
bindings/d/DEPENDENCIES.md | 4 +
bindings/d/DEPENDENCIES.rust.tsv | 0
bindings/d/README.md | 24 ++++
bindings/d/build.d | 42 +++++++
bindings/d/dscanner.ini | 105 ++++++++++++++++
bindings/d/dub.json | 37 ++++++
bindings/d/source/opendal/opendal_c.c | 23 ++++
bindings/d/source/opendal/operator.d | 229 ++++++++++++++++++++++++++++++++++
bindings/d/source/opendal/package.d | 123 ++++++++++++++++++
bindings/d/test/dub.json | 14 +++
bindings/d/test/source/bdd.d | 97 ++++++++++++++
19 files changed, 858 insertions(+)
diff --git a/.devcontainer/post_create.sh b/.devcontainer/post_create.sh
index cbc54dfeab..987da1a8c5 100644
--- a/.devcontainer/post_create.sh
+++ b/.devcontainer/post_create.sh
@@ -69,3 +69,6 @@ opam install -y dune ounit2 ocamlformat
# Setup for Cpp binding
sudo apt install -y ninja-build
+
+# Setup for D binding
+sudo apt install -y dmd dub
diff --git a/.gitattributes b/.gitattributes
index 51806da035..cf8f7fa77c 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -8,6 +8,7 @@ integrations export-ignore
bindings/c export-ignore
bindings/cpp export-ignore
+bindings/d export-ignore
bindings/dotnet export-ignore
bindings/go export-ignore
bindings/haskell export-ignore
diff --git a/.github/workflows/ci_bindings_d.yml
b/.github/workflows/ci_bindings_d.yml
new file mode 100644
index 0000000000..9835868b59
--- /dev/null
+++ b/.github/workflows/ci_bindings_d.yml
@@ -0,0 +1,73 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+name: Bindings D CI
+
+on:
+ push:
+ branches:
+ - main
+ tags:
+ - "*"
+ pull_request:
+ branches:
+ - main
+ paths:
+ - "core/**"
+ - "bindings/c/**"
+ - "bindings/d/**"
+ - ".github/workflows/ci_bindings_d.yml"
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ strategy:
+ matrix:
+ # dmd: base (self-hosting) compiler (frontend & backend)
+ # ldc2/ldmd2: (dmd-frontend + LLVM backend) - recommended for MacOS ARM64
+ dlang: ["ldc-latest", "dmd-latest"]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: dlang-community/setup-dlang@v2
+ with:
+ compiler: ${{ matrix.dlang }}
+
+ - name: Setup Rust toolchain
+ uses: ./.github/actions/setup
+
+ - name: Build D binding
+ working-directory: bindings/d
+ run: dub build
+
+ - name: Check diff
+ run: git diff --exit-code
+
+ - name: Check
+ working-directory: bindings/d
+ run: dub lint
+
+ - name: Run tests
+ working-directory: bindings/d
+ run: dub test && cd test && dub
diff --git a/.typos.toml b/.typos.toml
index 9c58e5d2d9..7d504be990 100644
--- a/.typos.toml
+++ b/.typos.toml
@@ -36,4 +36,6 @@ extend-exclude = [
# Generated pnpm locks.
"website/pnpm-lock.yaml",
"CHANGELOG.md",
+ # dscanner config
+ "bindings/d/dscanner.ini",
]
diff --git a/README.md b/README.md
index 4e74c654ea..d721fa888b 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,7 @@ OpenDAL offers a unified data access layer, empowering users
to seamlessly and e
| [Rust Core] | [![Rust Core Image]][Rust Core Link] |
[![Docs Release]][Rust Core Release Docs] [![Docs Dev]][Rust Core Dev Docs]
|
| [C Binding] | - |
[![Docs Dev]][C Binding Dev Docs]
|
| [Cpp Binding] | - |
[![Docs Dev]][Cpp Binding Dev Docs]
|
+| [D Binding] | - | -
|
| [Dotnet Binding] | - | -
|
| [Go Binding] | [![Go Binding Image]][Go Binding Link] |
[![Docs Release]][Go Release Docs] |
| [Haskell Binding] | - | -
|
@@ -38,6 +39,7 @@ OpenDAL offers a unified data access layer, empowering users
to seamlessly and e
[C Binding Dev Docs]: https://opendal.apache.org/docs/c/
[Cpp Binding]: bindings/cpp/README.md
[Cpp Binding Dev Docs]: https://opendal.apache.org/docs/cpp/
+[D Binding]: bindings/d/README.md
[Dotnet Binding]: bindings/dotnet/README.md
[Go Binding]: bindings/go/README.md
[Go Binding Image]:
https://badge.fury.io/go/github.com%2Fapache%2Fopendal%2Fbindings%2Fgo.svg
diff --git a/bindings/README.md b/bindings/README.md
index 21d32c7d5e..2ad662e693 100644
--- a/bindings/README.md
+++ b/bindings/README.md
@@ -13,6 +13,7 @@ This folder contains the bindings for OpenDAL. Currently, we
support the followi
* [C](c/README.md)
* [C++](cpp/README.md)
* [C#](dotnet/README.md)
+* [D](d/README.md)
* [Go](go/README.md)
* [Haskell](haskell/README.md)
* [Lua](lua/README.md)
diff --git a/bindings/d/.gitignore b/bindings/d/.gitignore
new file mode 100644
index 0000000000..3acdc490d3
--- /dev/null
+++ b/bindings/d/.gitignore
@@ -0,0 +1,20 @@
+.dub
+docs.json
+__dummy.html
+docs/
+/d
+d.so
+d.dylib
+d.dll
+d.a
+d.lib
+d-test-*
+*.exe
+*.pdb
+*.o
+*.obj
+*.lst
+*.h
+*.a
+*.ninja_log
+tests*
diff --git a/bindings/d/CONTRIBUTING.md b/bindings/d/CONTRIBUTING.md
new file mode 100644
index 0000000000..c64e596ad5
--- /dev/null
+++ b/bindings/d/CONTRIBUTING.md
@@ -0,0 +1,58 @@
+# Contributing
+
+- [Contributing](#contributing)
+ - [Setup](#setup)
+ - [Using a dev container environment](#using-a-dev-container-environment)
+ - [Bring your own toolbox](#bring-your-own-toolbox)
+ - [Build](#build)
+ - [Test](#test)
+
+## Setup
+
+### Using a dev container environment
+
+OpenDAL provides a pre-configured [dev container](https://containers.dev/)
that could be used in [GitHub
Codespaces](https://github.com/features/codespaces),
[VSCode](https://code.visualstudio.com/),
[JetBrains](https://www.jetbrains.com/remote-development/gateway/),
[JupyterLab](https://jupyterlab.readthedocs.io/en/stable/). Please pick up your
favourite runtime environment.
+
+The fastest way is:
+
+[](https://codespaces.new/apache/opendal?quickstart=1&machine=standardLinux32gb)
+
+### Bring your own toolbox
+
+To build OpenDAL D binding locally, you need:
+
+- [dmd/ldc/gdc](https://dlang.org/download)
+
+
+## Build
+
+First, build the C bindings:
+
+```shell
+dub build -b release
+```
+
+> **Note**:
+>
+> - `dub build` adds the header file `opendal.h` under `../c/include`
+> - The library is under `../../target/debug` or `../../target/release` after
building.
+
+## Test
+
+To build and run the tests.
+
+```shell
+$ dub test
+ Generating test runner configuration 'opendal-test-unittest' for 'unittest'
(library).
+ Starting Performing "unittest" build using /usr/bin/ldc2 for x86_64.
+ Building opendal ~master: building configuration [opendal-test-unittest]
+ Pre-build Running commands
+ Finished `release` profile [optimized] target(s) in 0.08s
+Cargo build completed successfully
+ Linking opendal-test-unittest
+ Running opendal-test-unittest
+Basic Operator creation and write test passed
+1 modules passed unittests
+```
+
+
diff --git a/bindings/d/DEPENDENCIES.md b/bindings/d/DEPENDENCIES.md
new file mode 100644
index 0000000000..9909723208
--- /dev/null
+++ b/bindings/d/DEPENDENCIES.md
@@ -0,0 +1,4 @@
+# Dependencies
+
+OpenDAL D Binding is based on the C Binding.
+There are no extra runtime dependencies except those conveyed from C Binding.
diff --git a/bindings/d/DEPENDENCIES.rust.tsv b/bindings/d/DEPENDENCIES.rust.tsv
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/bindings/d/README.md b/bindings/d/README.md
new file mode 100644
index 0000000000..bb1a9e40fd
--- /dev/null
+++ b/bindings/d/README.md
@@ -0,0 +1,24 @@
+# Apache OpenDAL™ D Binding (WIP)
+
+
+
+
+
+## Build
+
+To compile OpenDAL from source code, you need:
+
+- [dmd/ldc/gdc](https://dlang.org/download)
+
+```bash
+# build libopendal_c (underneath call make -C ../c)
+dub build -b release
+# build and run unit tests
+dub test
+```
+
+## License and Trademarks
+
+Licensed under the Apache License, Version 2.0:
http://www.apache.org/licenses/LICENSE-2.0
+
+Apache OpenDAL, OpenDAL, and Apache are either registered trademarks or
trademarks of the Apache Software Foundation.
diff --git a/bindings/d/build.d b/bindings/d/build.d
new file mode 100644
index 0000000000..9b5387a804
--- /dev/null
+++ b/bindings/d/build.d
@@ -0,0 +1,42 @@
+module build;
+import std.stdio: writeln;
+import std.path: buildPath;
+import std.process: spawnShell, wait;
+import std.exception;
+import std.file: copy, mkdir, exists;
+import std.conv: to;
+
+version (Windows)
+ enum staticlib = "opendal_c.lib";
+else
+ enum staticlib = "libopendal_c.a";
+
+void main (string[] args)
+{
+ bool isRelease = args.length > 1 && args[1] == "release";
+ string buildType = isRelease ? "release" : "debug";
+
+ // Run cargo build
+ auto cargoCmd = "cargo build --manifest-path " ~ buildPath(
+ "..", "c", "Cargo.toml") ~ (isRelease ? " --release" : "");
+
+ auto status = wait(spawnShell(cargoCmd));
+ if (status != 0)
+ {
+ throw new Exception("Cargo build failed with status: " ~
status.to!string);
+ }
+ else
+ {
+ writeln("Cargo build completed successfully");
+ }
+
+ // Get opendal.h
+ copy(buildPath("..", "c", "include", "opendal.h"), buildPath("source",
"opendal", "opendal.h"));
+
+ // Get libopendal_c.a
+ auto libPath = buildPath("..", "c", "target", buildType, staticlib);
+ writeln("Copying ", libPath, " to ", buildPath("lib", staticlib));
+ if (!exists("lib"))
+ mkdir ("lib");
+ copy(libPath, buildPath("lib", staticlib));
+}
diff --git a/bindings/d/dscanner.ini b/bindings/d/dscanner.ini
new file mode 100644
index 0000000000..48472d548c
--- /dev/null
+++ b/bindings/d/dscanner.ini
@@ -0,0 +1,105 @@
+; Configure which static analysis checks are enabled
+[analysis.config.StaticAnalysisConfig]
+; Check variable, class, struct, interface, union, and function names against
+; the Phobos style guide
+style_check="disabled"
+; Check for array literals that cause unnecessary allocation
+enum_array_literal_check="enabled"
+; Check for poor exception handling practices
+exception_check="enabled"
+; Check for use of the deprecated 'delete' keyword
+delete_check="enabled"
+; Check for use of the deprecated floating point operators
+float_operator_check="enabled"
+; Check underscores to improve number constant readability
+number_style_check="enabled"
+; Checks that opEquals, opCmp, toHash, and toString are either const, immutable
+; , or inout.
+object_const_check="enabled"
+; Checks for .. expressions where the left side is larger than the right.
+backwards_range_check="enabled"
+; Checks for if statements whose 'then' block is the same as the 'else' block
+if_else_same_check="enabled"
+; Checks for some problems with constructors
+constructor_check="enabled"
+; Checks for unused function parameters
+unused_parameter_check="disabled"
+; Checks for unused variables
+unused_variable_check="enabled"
+; Checks for unused labels
+unused_label_check="enabled"
+; Checks for duplicate attributes
+duplicate_attribute="enabled"
+; Checks that opEquals and toHash are both defined or neither are defined
+opequals_tohash_check="disabled"
+; Checks for subtraction from .length properties
+length_subtraction_check="disabled"
+; Checks for methods or properties whose names conflict with built-in propertie
+; s
+builtin_property_names_check="enabled"
+; Checks for confusing code in inline asm statements
+asm_style_check="enabled"
+; Checks for confusing logical operator precedence
+logical_precedence_check="disabled"
+; Checks for undocumented public declarations
+undocumented_declaration_check="disabled"
+; Checks for poor placement of function attributes
+function_attribute_check="enabled"
+; Checks for use of the comma operator
+comma_expression_check="enabled"
+; Checks for local imports that are too broad
+local_import_check="disabled"
+; Checks for variables that could be declared immutable
+could_be_immutable_check="disabled"
+; Checks for redundant expressions in if statements
+redundant_if_check="enabled"
+; Checks for redundant parenthesis
+redundant_parens_check="disabled"
+; Checks for mismatched argument and parameter names
+mismatched_args_check="disabled"
+; Checks for labels with the same name as variables
+label_var_same_name_check="disabled"
+; Checks for lines longer than 120 characters
+long_line_check="disabled"
+; Checks for assignment to auto-ref function parameters
+auto_ref_assignment_check="disabled"
+; Checks for incorrect infinite range definitions
+incorrect_infinite_range_check="enabled"
+; Checks for asserts that are always true
+useless_assert_check="enabled"
+; Check for uses of the old-style alias syntax
+alias_syntax_check="enabled"
+; Checks for else if that should be else static if
+static_if_else_check="enabled"
+; Check for unclear lambda syntax
+lambda_return_check="enabled"
+; Check for auto function without return statement
+auto_function_check="enabled"
+; Check for sortedness of imports
+imports_sortedness="disabled"
+; Check for explicitly annotated unittests
+explicitly_annotated_unittests="disabled"
+; Check for properly documented public functions (Returns, Params)
+properly_documented_public_functions="disabled"
+; Check for useless usage of the final attribute
+final_attribute_check="enabled"
+; Check for virtual calls in the class constructors
+vcall_in_ctor="enabled"
+; Check for useless user defined initializers
+useless_initializer="disabled"
+; Check allman brace style
+allman_braces_check="disabled"
+; Check for redundant attributes
+redundant_attributes_check="disabled"
+; Check for public declarations without a documented unittest
+has_public_example="disabled"
+; Check for asserts without an explanatory message
+assert_without_msg="disabled"
+; Check indent of if constraints
+if_constraints_indent="enabled"
+; Check for @trusted applied to a bigger scope than a single function
+trust_too_much="disabled"
+; Check for redundant storage classes on variable declarations")
+redundant_storage_classes="enabled"
+; Check for unused function return values
+unused_result="disabled"
\ No newline at end of file
diff --git a/bindings/d/dub.json b/bindings/d/dub.json
new file mode 100644
index 0000000000..668fc63f16
--- /dev/null
+++ b/bindings/d/dub.json
@@ -0,0 +1,37 @@
+{
+ "name": "opendal",
+ "license": "Apache-2.0",
+ "description": "Apache OpenDAL D bindings",
+ "excludedSourceFiles": ["source/opendal/package.d"],
+ "toolchainRequirements": {
+ "frontend": ">=2.105.3"
+ },
+ "buildTypes": {
+ "debug": {
+ "buildOptions": ["debugMode", "debugInfo"]
+ },
+ "release": {
+ "buildOptions": ["releaseMode", "optimize", "inline"]
+ }
+ },
+ "preBuildCommands": ["\"$DC\" -run $PACKAGE_DIR/build.d -- release"],
+ "configurations": [
+ {
+ "name": "opendal",
+ "targetType": "library",
+ "targetName": "opendal",
+ "sourceFiles": ["source/opendal/package.d"],
+ "targetPath": "lib",
+ "lflags-posix": ["-L$PACKAGE_DIR/lib"],
+ "lflags-windows": ["/LIBPATH:$PACKAGE_DIR\\lib"],
+ "libs": ["opendal_c"]
+ },
+ {
+ "name": "unittest",
+ "sourceFiles": ["source/opendal/package.d"],
+ "lflags-posix": ["-L$PACKAGE_DIR/lib"],
+ "lflags-windows": ["/LIBPATH:$PACKAGE_DIR\\lib"],
+ "libs": ["opendal_c"]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/bindings/d/source/opendal/opendal_c.c
b/bindings/d/source/opendal/opendal_c.c
new file mode 100644
index 0000000000..4f0d59e888
--- /dev/null
+++ b/bindings/d/source/opendal/opendal_c.c
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+/// OpenDAL - ImportC generate D bindings
+#pragma attribute(push, nogc, nothrow)
+#include "opendal.h"
+#pragma attribute(pop)
\ No newline at end of file
diff --git a/bindings/d/source/opendal/operator.d
b/bindings/d/source/opendal/operator.d
new file mode 100644
index 0000000000..a3fe616fbf
--- /dev/null
+++ b/bindings/d/source/opendal/operator.d
@@ -0,0 +1,229 @@
+/*
+ * 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.
+ */
+
+module opendal.operator;
+
+import std.string: toStringz;
+import std.exception: enforce;
+import std.conv: to;
+import std.parallelism: task, TaskPool;
+
+/// OpenDAL-C binding for D. (unsafe/@system)
+private import opendal.opendal_c;
+
+struct Operator
+{
+ private opendal_operator* op;
+ private TaskPool taskPool;
+ private bool enabledParallelism;
+
+ this(string scheme, OperatorOptions options, bool useParallel = false)
@trusted
+ {
+ auto result = opendal_operator_new(scheme.toStringz, options.options);
+ enforce(result.op !is null, "Failed to create Operator");
+ enforce(result.error is null, "Error in Operator");
+ op = result.op;
+ enabledParallelism = useParallel;
+
+ if (enabledParallelism)
+ taskPool = new TaskPool();
+ }
+
+ void write(string path, ubyte[] data) @trusted
+ {
+ opendal_bytes bytes = opendal_bytes(data.ptr, data.length,
data.length);
+ auto error = opendal_operator_write(op, path.toStringz, &bytes);
+ enforce(error is null, "Error writing data");
+ }
+
+ void writeParallel(string path, ubyte[] data) @safe
+ {
+ auto t = task!((Operator* op, string p, ubyte[] d) { op.write(p, d);
})(&this, path, data);
+ taskPool.put(t);
+ t.yieldForce();
+ }
+
+ ubyte[] readParallel(string path) @trusted
+ {
+ auto t = task!((Operator* op, string p) { return op.read(p); })(&this,
path);
+ taskPool.put(t);
+ return t.yieldForce();
+ }
+
+ Entry[] listParallel(string path) @trusted
+ {
+ auto t = task!((Operator* op, string p) { return op.list(p); })(&this,
path);
+ taskPool.put(t);
+ return t.yieldForce();
+ }
+
+ ubyte[] read(string path) @trusted
+ {
+ auto result = opendal_operator_read(op, path.toStringz);
+ enforce(result.error is null, "Error reading data");
+ scope (exit)
+ opendal_bytes_free(&result.data);
+ return result.data.data[0 .. result.data.len].dup;
+ }
+
+ void remove(string path) @trusted
+ {
+ auto error = opendal_operator_delete(op, path.toStringz);
+ enforce(error is null, "Error deleting object");
+ }
+
+ bool exists(string path) @trusted
+ {
+ auto result = opendal_operator_exists(op, path.toStringz);
+ enforce(result.error is null, "Error checking existence");
+ return result.exists;
+ }
+
+ Metadata stat(string path) @trusted
+ {
+ auto result = opendal_operator_stat(op, path.toStringz);
+ enforce(result.error is null, "Error getting metadata");
+ return Metadata(result.meta);
+ }
+
+ Entry[] list(string path) @trusted
+ {
+ auto result = opendal_operator_list(op, path.toStringz);
+ enforce(result.error is null, "Error listing objects");
+
+ Entry[] entries;
+ while (true)
+ {
+ auto next = opendal_lister_next(result.lister);
+ if (next.entry is null)
+ break;
+ entries ~= Entry(next.entry);
+ }
+ return entries;
+ }
+
+ void createDir(string path) @trusted
+ {
+ auto error = opendal_operator_create_dir(op, path.toStringz);
+ enforce(error is null, "Error creating directory");
+ }
+
+ void rename(string src, string dest) @trusted
+ {
+ auto error = opendal_operator_rename(op, src.toStringz,
dest.toStringz);
+ enforce(error is null, "Error renaming object");
+ }
+
+ void copy(string src, string dest) @trusted
+ {
+ auto error = opendal_operator_copy(op, src.toStringz, dest.toStringz);
+ enforce(error is null, "Error copying object");
+ }
+
+ ~this() @trusted
+ {
+ if (op !is null)
+ opendal_operator_free(op);
+ if (enabledParallelism)
+ taskPool.stop();
+ }
+}
+
+class OperatorOptions
+{
+ private opendal_operator_options* options;
+
+ this() @trusted
+ {
+ options = opendal_operator_options_new();
+ }
+
+ void set(string key, string value) @trusted
+ {
+ opendal_operator_options_set(options, key.toStringz, value.toStringz);
+ }
+
+ ~this() @trusted
+ {
+ if (options !is null)
+ opendal_operator_options_free(options);
+ }
+}
+
+struct Metadata
+{
+ private opendal_metadata* meta;
+
+ this(scope opendal_metadata* meta) @trusted pure
+ {
+ this.meta = meta;
+ }
+
+ ulong contentLength() @trusted
+ {
+ return opendal_metadata_content_length(meta);
+ }
+
+ bool isFile() @trusted
+ {
+ return opendal_metadata_is_file(meta);
+ }
+
+ bool isDir() @trusted
+ {
+ return opendal_metadata_is_dir(meta);
+ }
+
+ long lastModified() @trusted
+ {
+ return opendal_metadata_last_modified_ms(meta);
+ }
+
+ ~this() @trusted
+ {
+ if (meta !is null)
+ opendal_metadata_free(meta);
+ }
+}
+
+struct Entry
+{
+ private opendal_entry* entry;
+
+ this(opendal_entry* entry) pure @nogc
+ {
+ this.entry = entry;
+ }
+
+ string path()
+ {
+ return to!string(opendal_entry_path(entry));
+ }
+
+ string name()
+ {
+ return to!string(opendal_entry_name(entry));
+ }
+
+ ~this()
+ {
+ if (entry !is null)
+ opendal_entry_free(entry);
+ }
+}
diff --git a/bindings/d/source/opendal/package.d
b/bindings/d/source/opendal/package.d
new file mode 100644
index 0000000000..523b1ed8c6
--- /dev/null
+++ b/bindings/d/source/opendal/package.d
@@ -0,0 +1,123 @@
+/*
+ * 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.
+ */
+
+module opendal;
+
+public import opendal.operator;
+
+version (unittest)
+{
+ @("Test basic Operator creation")
+ @safe unittest
+ {
+ /* Initialize a operator for "memory" backend, with no options
*/
+ OperatorOptions options = new OperatorOptions();
+ Operator op = Operator("memory", options);
+
+ /* Prepare some data to be written */
+ string data = "this_string_length_is_24";
+
+ /* Write this into path "/testpath" */
+ op.write("/testpath", cast(ubyte[])data.dup);
+
+ /* We can read it out, make sure the data is the same */
+ auto read_bytes = op.read("/testpath");
+ assert(read_bytes.length == 24);
+ assert(cast(string)read_bytes.idup == data);
+ }
+
+ @("Benchmark parallel and normal functions")
+ @safe unittest
+ {
+ import std.exception: assertNotThrown;
+ import std.file: tempDir;
+ import std.path: buildPath;
+ import std.datetime.stopwatch: StopWatch;
+ import std.stdio: writeln;
+
+ auto options = new OperatorOptions();
+ options.set("root", tempDir);
+ auto op = Operator("fs", options, true);
+
+ auto testPath = buildPath(tempDir, "benchmark_test.txt");
+ auto testData = cast(ubyte[])"Benchmarking OpenDAL async and
normal functions".dup;
+
+ // Benchmark write operations
+ StopWatch sw;
+
+ sw.start();
+ assertNotThrown(op.write(testPath, testData));
+ sw.stop();
+ auto normalWriteTime = sw.peek();
+
+ sw.reset();
+ sw.start();
+ assertNotThrown(op.writeParallel(testPath, testData));
+ sw.stop();
+ auto parallelWriteTime = sw.peek();
+
+ // Benchmark read operations
+ sw.reset();
+ sw.start();
+ auto normalReadData = op.read(testPath);
+ sw.stop();
+ auto normalReadTime = sw.peek();
+
+ sw.reset();
+ sw.start();
+ auto parallelReadData = op.readParallel(testPath);
+ sw.stop();
+ auto parallelReadTime = sw.peek();
+
+ // Benchmark list operations
+ sw.reset();
+ sw.start();
+ op.list(tempDir);
+ sw.stop();
+ auto normalListTime = sw.peek();
+
+ sw.reset();
+ sw.start();
+ op.listParallel(tempDir);
+ sw.stop();
+ auto parallelListTime = sw.peek();
+
+ // Print benchmark results
+ writeln("Write benchmark:");
+ writeln(" Normal: ", normalWriteTime);
+ writeln(" Parallel: ", parallelWriteTime);
+
+ writeln("Read benchmark:");
+ writeln(" Normal: ", normalReadTime);
+ writeln(" Parallel: ", parallelReadTime);
+
+ writeln("List benchmark:");
+ writeln(" Normal: ", normalListTime);
+ writeln(" Parallel: ", parallelListTime);
+
+ // Verify data integrity
+ assert(normalReadData == testData);
+ assert(parallelReadData == testData);
+
+ // Clean up
+ op.remove(testPath);
+ assert(!op.exists(testPath));
+ }
+
+}
diff --git a/bindings/d/test/dub.json b/bindings/d/test/dub.json
new file mode 100644
index 0000000000..d21ccb1079
--- /dev/null
+++ b/bindings/d/test/dub.json
@@ -0,0 +1,14 @@
+{
+ "name": "tests",
+ "targetType": "executable",
+ "dflags": ["-preview=all"],
+ "dependencies": {"opendal":{"path": ".."}},
+ "buildTypes": {
+ "debug": {
+ "buildOptions": ["debugMode", "debugInfo"]
+ },
+ "release": {
+ "buildOptions": ["releaseMode", "optimize", "inline"]
+ }
+ }
+}
\ No newline at end of file
diff --git a/bindings/d/test/source/bdd.d b/bindings/d/test/source/bdd.d
new file mode 100644
index 0000000000..2658f40307
--- /dev/null
+++ b/bindings/d/test/source/bdd.d
@@ -0,0 +1,97 @@
+/*
+ * 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.
+ */
+module bdd;
+
+import opendal;
+import std.stdio: writeln;
+
+class OperatorContext
+{
+ Operator op;
+
+ this() @trusted
+ {
+ auto options = new OperatorOptions();
+ op = Operator("memory", options);
+ }
+}
+
+class WriteScenario
+{
+ OperatorContext context;
+ string data;
+ string path;
+
+ this(OperatorContext context) @trusted
+ {
+ this.context = context;
+ }
+
+ WriteScenario givenSomeData(string data)
+ {
+ this.data = data;
+ return this;
+ }
+
+ WriteScenario whenWritingToPath(string path)
+ {
+ this.path = path;
+ context.op.write(path, cast(ubyte[])data.dup);
+ return this;
+ }
+
+ void thenDataShouldBeReadable()
+ {
+ auto read_bytes = context.op.read(path);
+ assert(read_bytes.length == data.length, "Read data length does not
match written data length");
+ assert(cast(string)read_bytes.idup == data, "Read data does not match
written data");
+ }
+}
+
+void main() @safe
+{
+ auto context = new OperatorContext();
+
+ describe("Operator memory backend", {
+ it("should write and read data correctly", {
+ new WriteScenario(context)
+ .givenSomeData("this_string_length_is_24")
+ .whenWritingToPath("/testpath")
+ .thenDataShouldBeReadable();
+ });
+
+ it("should print the read data", {
+ auto read_bytes = context.op.read("/testpath");
+ writeln(cast(string)read_bytes.idup);
+ });
+ });
+}
+
+void describe(string description, void delegate() tests) @trusted
+{
+ writeln("Describe: ", description);
+ tests();
+}
+
+void it(string description, void delegate() test)
+{
+ writeln(" It ", description);
+ test();
+ writeln(" - Passed");
+}