Skip to content

Commit

Permalink
feat(bindings/go): Add full native support from C to Go. (#4886)
Browse files Browse the repository at this point in the history
Signed-off-by: Hanchin Hsieh <me@yuchanns.xyz>
  • Loading branch information
yuchanns authored Jul 13, 2024
1 parent 9ef494d commit bf15cec
Show file tree
Hide file tree
Showing 26 changed files with 4,020 additions and 162 deletions.
29 changes: 3 additions & 26 deletions .github/workflows/ci_bindings_go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,32 +46,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.20'

- name: Setup Rust toolchain
uses: ./.github/actions/setup

- name: Build c binding
working-directory: bindings/c
run: make build

- name: Check diff
run: git diff --exit-code

- name: Generate pkg-config file
run: |
echo "libdir=$(pwd)/bindings/c/target/debug/" >> opendal_c.pc
echo "includedir=$(pwd)/bindings/c/include/" >> opendal_c.pc
echo "Name: opendal_c" >> opendal_c.pc
echo "Description: opendal c binding" >> opendal_c.pc
echo "Version: 0.0.1" >> opendal_c.pc
echo "Libs: -L\${libdir} -lopendal_c" >> opendal_c.pc
echo "Cflags: -I\${includedir}" >> opendal_c.pc
echo "PKG_CONFIG_PATH=$(pwd)" >> $GITHUB_ENV
echo "LD_LIBRARY_PATH=$(pwd)/bindings/c/target/debug" >> $GITHUB_ENV

- name: Run tests
env:
OPENDAL_TEST: "memory"
working-directory: bindings/go
run: go test -tags dynamic .
run: CGO_ENABLE=0 go test -v -run TestBehavior
2 changes: 1 addition & 1 deletion bindings/go/DEPENDENCIES.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Dependencies

OpenDAL Go Binding is based on the C Binding.
There are no extra runtime dependencies except those conveyed from C Binding.
Installation of libffi is required.
131 changes: 97 additions & 34 deletions bindings/go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,123 @@

![](https://img.shields.io/badge/status-unreleased-red)

opendal-go requires opendal-c to be installed.
opendal-go is a **Native** support Go binding without CGO enabled and is built on top of opendal-c.

```shell
cd bindings/c
make build
```bash
go get github.com/apache/opendal/bindings/go@latest
```

You will find `libopendal_c.so` under `{root}/target`.
opendal-go requires **libffi** to be installed.

Then, we need to add a `opendal_c.pc` files
## Basic Usage

```pc
libdir=/path/to/opendal/target/debug/
includedir=/path/to/opendal/bindings/c/include/
```go
package main

Name: opendal_c
Description: opendal c binding
Version:
import (
"fmt"
"os"

Libs: -L${libdir} -lopendal_c
Cflags: -I${includedir}
```
"github.com/yuchanns/opendal-go-services/memory"
"github.com/apache/opendal/bindings/go"
)

And set the `PKG_CONFIG_PATH` environment variable to the directory where `opendal_c.pc` is located.
func main() {
// Initialize a new in-memory operator
op, err := opendal.NewOperator(memory.Scheme, opendal.OperatorOptions{})
if err != nil {
panic(err)
}
defer op.Close()

```shell
export PKG_CONFIG_PATH=/dir/of/opendal_c.pc
```
// Write data to a file named "test"
err = op.Write("test", []byte("Hello opendal go binding!"))
if err != nil {
panic(err)
}

Then, we can build the go binding.
// Read data from the file "test"
data, err := op.Read("test")
if err != nil {
panic(err)
}
fmt.Printf("Read content: %s\n", data)

```shell
cd bindings/go
go build -tags dynamic .
```
// List all entries under the root directory "/"
lister, err := op.List("/")
if err != nil {
panic(err)
}
defer lister.Close()

To running the go binding tests, we need to tell the linker where to find the `libopendal_c.so` file.
// Iterate through all entries
for lister.Next() {
entry := lister.Entry()

```shell
expose LD_LIBRARY_PATH=/path/to/opendal/bindings/c/target/debug/
```
// Get entry name (not used in this example)
_ = entry.Name()

// Get metadata for the current entry
meta, _ := op.Stat(entry.Path())

// Print file size
fmt.Printf("Size: %d bytes\n", meta.ContentLength())

Then, we can run the tests.
// Print last modified time
fmt.Printf("Last modified: %s\n", meta.LastModified())

```shell
go test -tags dynamic .
// Check if the entry is a directory or a file
fmt.Printf("Is directory: %v, Is file: %v\n", meta.IsDir(), meta.IsFile())
}

// Check for any errors that occurred during iteration
if err := lister.Error(); err != nil {
panic(err)
}

// Copy a file
op.Copy("test", "test_copy")

// Rename a file
op.Rename("test", "test_rename")

// Delete a file
op.Delete("test_rename")
}
```

For benchmark
## Run Tests

```shell
go test -bench=. -tags dynamic .
```bash
# Run all tests
CGO_ENABLE=0 go test -v -run TestBehavior
# Run specific test
CGO_ENABLE=0 go test -v -run TestBehavior/Write
# Run synchronously
CGO_ENABLE=0 GOMAXPROCS=1 go test -v -run TestBehavior
```

## Capabilities

- [x] OperatorInfo
- [x] Stat
- [x] Metadata
- [x] IsExist
- [x] Read
- [x] Read
- [x] Reader -- implement as `io.ReadCloser`
- [ ] Write
- [x] Write
- [ ] Writer -- Need support from the C binding
- [x] Delete
- [x] CreateDir
- [ ] Lister
- [x] Entry
- [ ] Metadata -- Need support from the C binding
- [x] Copy
- [x] Rename


## License and Trademarks

Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
Expand Down
158 changes: 158 additions & 0 deletions bindings/go/copy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* 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.
*/

package opendal_test

import (
"fmt"

"github.com/apache/opendal/bindings/go"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)

func testsCopy(cap *opendal.Capability) []behaviorTest {
if !cap.Read() || !cap.Write() || !cap.Copy() {
return nil
}
return []behaviorTest{
testCopyFileWithASCIIName,
testCopyFileWithNonASCIIName,
testCopyNonExistingSource,
testCopySourceDir,
testCopyTargetDir,
testCopySelf,
testCopyNested,
testCopyOverwrite,
}
}

func testCopyFileWithASCIIName(assert *require.Assertions, op *opendal.Operator, fixture *fixture) {
sourcePath, sourceContent, _ := fixture.NewFile()

assert.Nil(op.Write(sourcePath, sourceContent))

targetPath := fixture.NewFilePath()

assert.Nil(op.Copy(sourcePath, targetPath))

targetContent, err := op.Read(targetPath)
assert.Nil(err, "read must succeed")
assert.Equal(sourceContent, targetContent)
}

func testCopyFileWithNonASCIIName(assert *require.Assertions, op *opendal.Operator, fixture *fixture) {
sourcePath, sourceContent, _ := fixture.NewFileWithPath("🐂🍺中文.docx")
targetPath := fixture.PushPath("😈🐅Français.docx")

assert.Nil(op.Write(sourcePath, sourceContent))
assert.Nil(op.Copy(sourcePath, targetPath))

targetContent, err := op.Read(targetPath)
assert.Nil(err, "read must succeed")
assert.Equal(sourceContent, targetContent)
}

func testCopyNonExistingSource(assert *require.Assertions, op *opendal.Operator, _ *fixture) {
sourcePath := uuid.NewString()
targetPath := uuid.NewString()

err := op.Copy(sourcePath, targetPath)
assert.NotNil(err, "copy must fail")
assert.Equal(opendal.CodeNotFound, assertErrorCode(err))
}

func testCopySourceDir(assert *require.Assertions, op *opendal.Operator, fixture *fixture) {
if !op.Info().GetFullCapability().CreateDir() {
return
}

sourcePath := fixture.NewDirPath()
targetPath := uuid.NewString()

assert.Nil(op.CreateDir(sourcePath))

err := op.Copy(sourcePath, targetPath)
assert.NotNil(err, "copy must fail")
assert.Equal(opendal.CodeIsADirectory, assertErrorCode(err))
}

func testCopyTargetDir(assert *require.Assertions, op *opendal.Operator, fixture *fixture) {
if !op.Info().GetFullCapability().CreateDir() {
return
}

sourcePath, sourceContent, _ := fixture.NewFile()

assert.Nil(op.Write(sourcePath, sourceContent))

targetPath := fixture.NewDirPath()

assert.Nil(op.CreateDir(targetPath))

err := op.Copy(sourcePath, targetPath)
assert.NotNil(err, "copy must fail")
assert.Equal(opendal.CodeIsADirectory, assertErrorCode(err))
}

func testCopySelf(assert *require.Assertions, op *opendal.Operator, fixture *fixture) {
sourcePath, sourceContent, _ := fixture.NewFile()

assert.Nil(op.Write(sourcePath, sourceContent))

err := op.Copy(sourcePath, sourcePath)
assert.NotNil(err, "copy must fail")
assert.Equal(opendal.CodeIsSameFile, assertErrorCode(err))
}

func testCopyNested(assert *require.Assertions, op *opendal.Operator, fixture *fixture) {
sourcePath, sourceContent, _ := fixture.NewFile()

assert.Nil(op.Write(sourcePath, sourceContent))

targetPath := fixture.PushPath(fmt.Sprintf(
"%s/%s/%s",
uuid.NewString(),
uuid.NewString(),
uuid.NewString(),
))

assert.Nil(op.Copy(sourcePath, targetPath))

targetContent, err := op.Read(targetPath)
assert.Nil(err, "read must succeed")
assert.Equal(sourceContent, targetContent)
}

func testCopyOverwrite(assert *require.Assertions, op *opendal.Operator, fixture *fixture) {
sourcePath, sourceContent, _ := fixture.NewFile()

assert.Nil(op.Write(sourcePath, sourceContent))

targetPath, targetContent, _ := fixture.NewFile()
assert.NotEqual(sourceContent, targetContent)

assert.Nil(op.Write(targetPath, targetContent))

assert.Nil(op.Copy(sourcePath, targetPath))

targetContent, err := op.Read(targetPath)
assert.Nil(err, "read must succeed")
assert.Equal(sourceContent, targetContent)
}
Loading

0 comments on commit bf15cec

Please sign in to comment.