diff --git a/Cargo.lock b/Cargo.lock index eb62479b5383..c57103aefd2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2275,6 +2275,7 @@ dependencies = [ name = "opendal-c" version = "0.1.0" dependencies = [ + "bytes", "cbindgen", "opendal", ] diff --git a/bindings/c/Cargo.toml b/bindings/c/Cargo.toml index 9d39b43ecad4..0992763def66 100644 --- a/bindings/c/Cargo.toml +++ b/bindings/c/Cargo.toml @@ -35,4 +35,5 @@ doc = false cbindgen = "0.24.0" [dependencies] +bytes = "1.4.0" opendal = { version = "0.30", path = "../../core" } diff --git a/bindings/c/Makefile b/bindings/c/Makefile index f949cc09169a..28e391c3e4b7 100644 --- a/bindings/c/Makefile +++ b/bindings/c/Makefile @@ -19,7 +19,7 @@ RPATH=$(PWD)/../../target/debug CFLAGS = -I./include LDFLAGS = -L$(RPATH) -Wl,-rpath,$(RPATH) LIBS = -lopendal_c -OBJ_DIR=build +OBJ_DIR=./build .PHONY: all all: build test @@ -31,9 +31,9 @@ build: .PHONY: test test: - $(CC) tests/hello.c -o $(OBJ_DIR)/hello $(CFLAGS) $(LDFLAGS) $(LIBS) + $(CC) tests/basicio.c -o $(OBJ_DIR)/basicio $(CFLAGS) $(LDFLAGS) $(LIBS) .PHONY: clean clean: cargo clean - rm -rf "./$(OBJ_DIR)" + rm -rf $(OBJ_DIR) diff --git a/bindings/c/include/opendal.h b/bindings/c/include/opendal.h index 3cd506b4f2b0..eaec9f68b2cb 100644 --- a/bindings/c/include/opendal.h +++ b/bindings/c/include/opendal.h @@ -25,14 +25,147 @@ #include #include +/* + The error code for opendal APIs in C binding + */ +typedef enum opendal_code { + /* + All is well + */ + OPENDAL_OK, + /* + General error + */ + OPENDAL_ERROR, + /* + returning it back. For example, s3 returns an internal service error. + */ + OPENDAL_UNEXPECTED, + /* + Underlying service doesn't support this operation. + */ + OPENDAL_UNSUPPORTED, + /* + The config for backend is invalid. + */ + OPENDAL_CONFIG_INVALID, + /* + The given path is not found. + */ + OPENDAL_NOT_FOUND, + /* + The given path doesn't have enough permission for this operation + */ + OPENDAL_PERMISSION_DENIED, + /* + The given path is a directory. + */ + OPENDAL_IS_A_DIRECTORY, + /* + The given path is not a directory. + */ + OPENDAL_NOT_A_DIRECTORY, + /* + The given path already exists thus we failed to the specified operation on it. + */ + OPENDAL_ALREADY_EXISTS, + /* + Requests that sent to this path is over the limit, please slow down. + */ + OPENDAL_RATE_LIMITED, + /* + The given file paths are same. + */ + OPENDAL_IS_SAME_FILE, +} opendal_code; + +/* + The [`OperatorPtr`] owns a pointer to a [`od::BlockingOperator`]. + It is also the key struct that OpenDAL's APIs access the real + operator's memory. The use of OperatorPtr is zero cost, it + only returns a reference of the underlying Operator. + + The [`OperatorPtr`] also has a transparent layout, allowing you + to check its validity by native boolean operator. + e.g. you could check by (!ptr) on a opendal_operator_ptr type + */ +typedef const void *opendal_operator_ptr; + +/* + The [`Bytes`] type is a C-compatible substitute for [`Bytes`] + in Rust, it will not be deallocated automatically like what + has been done in Rust. Instead, you have to call [`opendal_free_bytes`] + to free the heap memory to avoid memory leak. + The field `data` should not be modified since it might causes + the reallocation of the Vector. + */ +typedef struct opendal_bytes { + const uint8_t *data; + uintptr_t len; +} opendal_bytes; + +/* + The Rust-like Result type of opendal C binding, it contains + the data that the read operation returns and a error code + If the read operation failed, the `data` fields should be a nullptr + and the error code is NOT OPENDAL_OK. + */ +typedef struct opendal_result_read { + struct opendal_bytes *data; + enum opendal_code code; +} opendal_result_read; + #ifdef __cplusplus extern "C" { #endif // __cplusplus /* - Hello, OpenDAL! + Returns a result type [`opendal_result_op`], with operator_ptr. If the construction succeeds + the error is nullptr, otherwise it contains the error information. + + # Safety + + It is [safe] under two cases below + * The memory pointed to by `scheme` must contain a valid nul terminator at the end of + the string. + * The `scheme` points to NULL, this function simply returns you a null opendal_operator_ptr + */ +opendal_operator_ptr opendal_operator_new(const char *scheme); + +/* + Write the data into the path blockingly by operator, returns the error code OPENDAL_OK + if succeeds, others otherwise + + # Safety + + It is [safe] under two cases below + * The memory pointed to by `path` must contain a valid nul terminator at the end of + the string. + * The `path` points to NULL, this function simply returns you false + */ +enum opendal_code opendal_operator_blocking_write(opendal_operator_ptr op_ptr, + const char *path, + struct opendal_bytes bytes); + +/* + Read the data out from path into a [`Bytes`] blockingly by operator, returns + a result with error code. If the error code is not OPENDAL_OK, the `data` field + of the result points to NULL. + + # Safety + + It is [safe] under two cases below + * The memory pointed to by `path` must contain a valid nul terminator at the end of + the string. + * The `path` points to NULL, this function simply returns you a nullptr + */ +struct opendal_result_read opendal_operator_blocking_read(opendal_operator_ptr op_ptr, + const char *path); + +/* + Frees the heap memory used by the [`Bytes`] */ -void hello_opendal(void); +void opendal_bytes_free(const struct opendal_bytes *vec); #ifdef __cplusplus } // extern "C" diff --git a/bindings/c/src/error.rs b/bindings/c/src/error.rs new file mode 100644 index 000000000000..61696a061728 --- /dev/null +++ b/bindings/c/src/error.rs @@ -0,0 +1,81 @@ +// 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 ::opendal as od; + +/// The wrapper type for opendal's error, wrapped because of the +/// orphan rule +pub struct opendal_error(od::Error); + +/// The error code for opendal APIs in C binding +#[repr(C)] +pub enum opendal_code { + /// All is well + OPENDAL_OK, + /// General error + // todo: make details in the `opendal_error *` + OPENDAL_ERROR, + /// returning it back. For example, s3 returns an internal service error. + OPENDAL_UNEXPECTED, + /// Underlying service doesn't support this operation. + OPENDAL_UNSUPPORTED, + /// The config for backend is invalid. + OPENDAL_CONFIG_INVALID, + /// The given path is not found. + OPENDAL_NOT_FOUND, + /// The given path doesn't have enough permission for this operation + OPENDAL_PERMISSION_DENIED, + /// The given path is a directory. + OPENDAL_IS_A_DIRECTORY, + /// The given path is not a directory. + OPENDAL_NOT_A_DIRECTORY, + /// The given path already exists thus we failed to the specified operation on it. + OPENDAL_ALREADY_EXISTS, + /// Requests that sent to this path is over the limit, please slow down. + OPENDAL_RATE_LIMITED, + /// The given file paths are same. + OPENDAL_IS_SAME_FILE, +} + +impl opendal_code { + pub(crate) fn from_opendal_error(e: od::Error) -> Self { + let error = opendal_error(e); + error.error_code() + } +} + +impl opendal_error { + /// Convert the [`od::ErrorKind`] of [`od::Error`] to [`opendal_code`] + pub(crate) fn error_code(&self) -> opendal_code { + let e = &self.0; + match e.kind() { + od::ErrorKind::Unexpected => opendal_code::OPENDAL_UNEXPECTED, + od::ErrorKind::Unsupported => opendal_code::OPENDAL_UNSUPPORTED, + od::ErrorKind::ConfigInvalid => opendal_code::OPENDAL_CONFIG_INVALID, + od::ErrorKind::NotFound => opendal_code::OPENDAL_NOT_FOUND, + od::ErrorKind::PermissionDenied => opendal_code::OPENDAL_PERMISSION_DENIED, + od::ErrorKind::IsADirectory => opendal_code::OPENDAL_IS_A_DIRECTORY, + od::ErrorKind::NotADirectory => opendal_code::OPENDAL_NOT_A_DIRECTORY, + od::ErrorKind::AlreadyExists => opendal_code::OPENDAL_ALREADY_EXISTS, + od::ErrorKind::RateLimited => opendal_code::OPENDAL_RATE_LIMITED, + od::ErrorKind::IsSameFile => opendal_code::OPENDAL_IS_SAME_FILE, + // if this is triggered, check the [`core`] crate and add a + // new error code accordingly + _ => panic!("The newly added ErrorKind in core crate is not handled in C bindings"), + } + } +} diff --git a/bindings/c/src/lib.rs b/bindings/c/src/lib.rs index 814db98caa67..8259f476b5a3 100644 --- a/bindings/c/src/lib.rs +++ b/bindings/c/src/lib.rs @@ -15,12 +15,126 @@ // specific language governing permissions and limitations // under the License. -use opendal::services::Memory; -use opendal::Operator; +#![allow(non_camel_case_types)] -/// Hello, OpenDAL! +mod error; +mod macros; +mod result; +mod types; + +use std::collections::HashMap; +use std::os::raw::c_char; +use std::str::FromStr; + +use crate::types::{opendal_bytes, opendal_operator_ptr}; + +use ::opendal as od; +use error::opendal_code; +use result::opendal_result_read; + +/// Returns a result type [`opendal_result_op`], with operator_ptr. If the construction succeeds +/// the error is nullptr, otherwise it contains the error information. +/// +/// # Safety +/// +/// It is [safe] under two cases below +/// * The memory pointed to by `scheme` must contain a valid nul terminator at the end of +/// the string. +/// * The `scheme` points to NULL, this function simply returns you a null opendal_operator_ptr #[no_mangle] -pub extern "C" fn hello_opendal() { - let op = Operator::new(Memory::default()).unwrap().finish(); - println!("{op:?}") +pub unsafe extern "C" fn opendal_operator_new(scheme: *const c_char) -> opendal_operator_ptr { + if scheme.is_null() { + return opendal_operator_ptr::null(); + } + + let scheme_str = unsafe { std::ffi::CStr::from_ptr(scheme).to_str().unwrap() }; + let scheme = match od::Scheme::from_str(scheme_str) { + Ok(s) => s, + Err(_) => { + return opendal_operator_ptr::null(); + } + }; + + // todo: api for map construction + let map = HashMap::default(); + + let op = match scheme { + od::Scheme::Memory => generate_operator!(od::services::Memory, map), + _ => { + return opendal_operator_ptr::null(); + } + } + .blocking(); + + // this prevents the operator memory from being dropped by the Box + let op = Box::leak(Box::new(op)); + + opendal_operator_ptr::from(op) +} + +/// Write the data into the path blockingly by operator, returns the error code OPENDAL_OK +/// if succeeds, others otherwise +/// +/// # Safety +/// +/// It is [safe] under two cases below +/// * The memory pointed to by `path` must contain a valid nul terminator at the end of +/// the string. +/// * The `path` points to NULL, this function simply returns you false +#[no_mangle] +pub unsafe extern "C" fn opendal_operator_blocking_write( + op_ptr: opendal_operator_ptr, + path: *const c_char, + bytes: opendal_bytes, +) -> opendal_code { + if path.is_null() { + return opendal_code::OPENDAL_ERROR; + } + + let op = op_ptr.get_ref(); + let path = unsafe { std::ffi::CStr::from_ptr(path).to_str().unwrap() }; + match op.write(path, bytes) { + Ok(_) => opendal_code::OPENDAL_OK, + Err(e) => opendal_code::from_opendal_error(e), + } +} + +/// Read the data out from path into a [`Bytes`] blockingly by operator, returns +/// a result with error code. If the error code is not OPENDAL_OK, the `data` field +/// of the result points to NULL. +/// +/// # Safety +/// +/// It is [safe] under two cases below +/// * The memory pointed to by `path` must contain a valid nul terminator at the end of +/// the string. +/// * The `path` points to NULL, this function simply returns you a nullptr +#[no_mangle] +pub unsafe extern "C" fn opendal_operator_blocking_read( + op_ptr: opendal_operator_ptr, + path: *const c_char, +) -> opendal_result_read { + if path.is_null() { + return opendal_result_read { + data: std::ptr::null_mut(), + code: opendal_code::OPENDAL_ERROR, + }; + } + + let op = op_ptr.get_ref(); + let path = unsafe { std::ffi::CStr::from_ptr(path).to_str().unwrap() }; + let data = op.read(path); + match data { + Ok(d) => { + let v = Box::new(opendal_bytes::from_vec(d)); + opendal_result_read { + data: Box::into_raw(v), + code: opendal_code::OPENDAL_OK, + } + } + Err(e) => opendal_result_read { + data: std::ptr::null_mut(), + code: opendal_code::from_opendal_error(e), + }, + } } diff --git a/bindings/c/src/macros.rs b/bindings/c/src/macros.rs new file mode 100644 index 000000000000..f77811d1abe1 --- /dev/null +++ b/bindings/c/src/macros.rs @@ -0,0 +1,31 @@ +// 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. + +/// Macro used to generate operator upon construction and return C-compatible +/// error if failed +#[macro_export] +macro_rules! generate_operator { + ($type:ty, $map:expr) => {{ + let b = od::Operator::from_map::<$type>($map); + match b { + Ok(b) => b.finish(), + Err(_) => { + return opendal_operator_ptr::null(); + } + } + }}; +} diff --git a/bindings/c/src/result.rs b/bindings/c/src/result.rs new file mode 100644 index 000000000000..fe69dd35de25 --- /dev/null +++ b/bindings/c/src/result.rs @@ -0,0 +1,33 @@ +// 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. + +//! This is for better naming in C header file. If we use generics for Result type, +//! it will no doubt work find. However, the generics will lead to naming like +//! "opendal_result_opendal_operator_ptr", which is unacceptable. Therefore, +//! we are defining all Result types here + +use crate::{error::opendal_code, types::opendal_bytes}; + +/// The Rust-like Result type of opendal C binding, it contains +/// the data that the read operation returns and a error code +/// If the read operation failed, the `data` fields should be a nullptr +/// and the error code is NOT OPENDAL_OK. +#[repr(C)] +pub struct opendal_result_read { + pub data: *mut opendal_bytes, + pub code: opendal_code, +} diff --git a/bindings/c/src/types.rs b/bindings/c/src/types.rs new file mode 100644 index 000000000000..dd03546f1ca6 --- /dev/null +++ b/bindings/c/src/types.rs @@ -0,0 +1,110 @@ +// 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::os::raw::c_void; + +use ::opendal as od; + +/// The [`OperatorPtr`] owns a pointer to a [`od::BlockingOperator`]. +/// It is also the key struct that OpenDAL's APIs access the real +/// operator's memory. The use of OperatorPtr is zero cost, it +/// only returns a reference of the underlying Operator. +/// +/// The [`OperatorPtr`] also has a transparent layout, allowing you +/// to check its validity by native boolean operator. +/// e.g. you could check by (!ptr) on a opendal_operator_ptr type +#[repr(transparent)] +pub struct opendal_operator_ptr { + // this is typed with [`c_void`] because cbindgen does not + // support our own custom type. + ptr: *const c_void, +} + +impl opendal_operator_ptr { + /// Creates an OperatorPtr will nullptr, indicating this [`OperatorPtr`] + /// is invalid. The `transparent` layout also guarantees that if the + /// underlying field `ptr` is a nullptr, the [`OperatorPtr`] has the + /// same layout as the nullptr. + pub(crate) fn null() -> Self { + Self { + ptr: std::ptr::null(), + } + } + + /// Returns a reference to the underlying [`BlockingOperator`] + pub(crate) fn get_ref(&self) -> &od::BlockingOperator { + unsafe { &*(self.ptr as *const od::BlockingOperator) } + } +} + +#[allow(clippy::from_over_into)] +impl From<&od::BlockingOperator> for opendal_operator_ptr { + fn from(value: &od::BlockingOperator) -> Self { + Self { + ptr: value as *const _ as *const c_void, + } + } +} + +#[allow(clippy::from_over_into)] +impl From<&mut od::BlockingOperator> for opendal_operator_ptr { + fn from(value: &mut od::BlockingOperator) -> Self { + Self { + ptr: value as *const _ as *const c_void, + } + } +} + +/// The [`Bytes`] type is a C-compatible substitute for [`Bytes`] +/// in Rust, it will not be deallocated automatically like what +/// has been done in Rust. Instead, you have to call [`opendal_free_bytes`] +/// to free the heap memory to avoid memory leak. +/// The field `data` should not be modified since it might causes +/// the reallocation of the Vector. +#[repr(C)] +pub struct opendal_bytes { + pub data: *const u8, + pub len: usize, +} + +impl opendal_bytes { + /// Construct a [`Vector`] from the Rust [`Vec`] of bytes + pub(crate) fn from_vec(vec: Vec) -> Self { + let data = vec.as_ptr() as *const u8; + let len = vec.len(); + std::mem::forget(vec); // To avoid deallocation of the vec. + Self { data, len } + } +} + +#[allow(clippy::from_over_into)] +impl Into for opendal_bytes { + fn into(self) -> bytes::Bytes { + let slice = unsafe { std::slice::from_raw_parts(self.data, self.len) }; + bytes::Bytes::from_static(slice) + } +} + +/// Frees the heap memory used by the [`Bytes`] +#[no_mangle] +pub extern "C" fn opendal_bytes_free(vec: *const opendal_bytes) { + unsafe { + // this deallocates the vector by reconstructing the vector and letting + // it be dropped when its out of scope + Vec::from_raw_parts((*vec).data as *mut u8, (*vec).len, (*vec).len); + } +} diff --git a/bindings/c/tests/basicio.c b/bindings/c/tests/basicio.c new file mode 100644 index 000000000000..a2e44ef3c1be --- /dev/null +++ b/bindings/c/tests/basicio.c @@ -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 "assert.h" +#include "stdio.h" +#include "opendal.h" + +void test_operator_rw(opendal_operator_ptr ptr) { + // have to be valid ptr + assert(ptr); + + // write some contents by the operator, must be successful + char path[] = "test"; + char content[] = "Hello World"; + const opendal_bytes data = { + .len = sizeof(content) - 1, + .data = (uint8_t*)content, + }; + opendal_code code = opendal_operator_blocking_write(ptr, path, data); + assert(code == OPENDAL_OK); + + // reads the data out from the bytes, must be successful + struct opendal_result_read r = opendal_operator_blocking_read(ptr, path); + assert(r.code == OPENDAL_OK); + assert(r.data->len == (sizeof(content) - 1)); + + for (int i = 0; i < r.data->len; i++) { + printf("%c", (char)(r.data->data[i])); + } + + // free the bytes's heap memory + opendal_bytes_free(r.data); +} + +int main(int argc, char *argv[]) { + // test memory operator + char scheme1[] = "memory"; + opendal_operator_ptr p1 = opendal_operator_new(scheme1); + assert(p1); + test_operator_rw(p1); + + return 0; +} diff --git a/bindings/c/tests/hello.c b/bindings/c/tests/hello.c deleted file mode 100644 index 4a6d921d468e..000000000000 --- a/bindings/c/tests/hello.c +++ /dev/null @@ -1,24 +0,0 @@ -/** - * 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.h" - -int main(int argc, char *argv[]) { - hello_opendal(); - return 0; -}