-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
131 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,132 @@ | ||
# goblend | ||
CGO-less FFI for Go | ||
CGO-less FFI for Go. | ||
|
||
goblend uses the `.syso` trick from [GcToolchainTricks](https://go.dev/wiki/GcToolchainTricks), to enable the | ||
embedding of object files using only the inbuilt Go linker. When combined with a small amount of trampoline assembly, | ||
this allows programs to use pre-compiled code from another language without requiring a cross-compiler. | ||
|
||
The main motivation behind this library is to enable library authors to reuse native code, without forcing downstream | ||
users to modify their build steps. Provided a user is on a supported platform, a goblend library can be used like any | ||
other Go library, with a simple `go get $pkg`. | ||
|
||
The main downside is that all non-Go code must be pre-compiled outside of `go build`, and for library authors, must be | ||
commited to the repository. The friction of using native code is shifted from the end user to the library maintainers. | ||
|
||
## Features | ||
- CGO-less FFI calls and callbacks (`CGO_ENABLED=0`) | ||
- Cross-compilation | ||
- e.g. Build for Windows from a Linux machine, without configuring a cross-compiler. | ||
- CGO compatible (`CGO_ENABLED=1`) | ||
- `runtime.iscgo == true` on all platforms | ||
- The runtime is built from the original CGO source (for `.S` and `.c` files) for all supported | ||
platforms. | ||
- Should be (virtually) indistinguishable from CGO. | ||
|
||
## Supported platforms | ||
|
||
- Linux: arm64 | ||
- Windows: amd64 | ||
- Darwin: arm64 | ||
|
||
## Example | ||
|
||
See `examples/add` for a complete example. | ||
|
||
To demonstrate the library is compatible: | ||
```bash | ||
$ go mod init example | ||
$ go get github.com/csnewman/goblend/examples/add | ||
$ go run github.com/csnewman/goblend/examples/add | ||
2024/12/09 11:23:39 Add example | ||
Hello world from Zig! | ||
2024/12/09 11:23:39 123 | ||
2024/12/09 11:23:39 End of example | ||
``` | ||
|
||
### Native code | ||
|
||
The exported functions must have the following signature: | ||
|
||
```zig | ||
export fn add(ptr: *const anyopaque) callconv(.c) u32 { | ||
// code here | ||
} | ||
``` | ||
|
||
and should be compiled to a native object file (archives/`.a` and libraries/`.dll` are unsupported): | ||
|
||
```bash | ||
zig build-obj -O ReleaseFast -target aarch64-linux-gnu --library c -femit-bin=core_linux_arm64.syso add.zig add2.zig etc | ||
``` | ||
|
||
The native object file should have the `.syso` extension, so the Go linker detects them. On Windows and Linux you can | ||
take advantage of partial linking to reduce the number of object files down to one per platform. | ||
|
||
### Native imports | ||
|
||
When building, you will likely experience errors such as: | ||
|
||
``` | ||
github.com/csnewman/goblend/runtime(.text): unknown symbol memset in callarm64 | ||
github.com/csnewman/goblend/runtime(.text): unknown symbol memset in callarm64 | ||
github.com/csnewman/goblend/runtime(.text): relocation target memset not defined | ||
``` | ||
|
||
This is caused by your native code importing other code, such as `memset` from `libc` in the above example. As such, you | ||
will need to create a file per platform, such as `core_linux.go` that imports the necessary dependencies: | ||
|
||
```Go | ||
//go:build linux | ||
|
||
package runtime | ||
|
||
import "unsafe" | ||
|
||
//go:cgo_import_dynamic memset memset "libc.so.6" | ||
|
||
// More dependencies... | ||
``` | ||
|
||
Note: On Windows, the situation is slightly more complex due to `__imp_{some name}` symbols being a pointer to a | ||
function, while the `{some name}` symbol expects to be the function itself. Please see `WriteFile` in the `examples/add` | ||
example. | ||
|
||
### Calling from Go | ||
|
||
First you will need to create a `trampoline` for each function you intend to call: (`core.s`) | ||
|
||
```asm | ||
#include "go_asm.h" | ||
#include "funcdata.h" | ||
#include "textflag.h" | ||
TEXT goblend_add(SB),NOSPLIT|NOFRAME,$0-0 | ||
JMP add(SB) | ||
RET | ||
``` | ||
|
||
Note, no parameter information is necessary here, as this is a simple `jmp`. | ||
|
||
Then in a Go file, such as `core.go`, you will need to get a ptr to the trampoline: | ||
|
||
```go | ||
package main | ||
|
||
import "unsafe" | ||
|
||
//go:linkname add goblend_add | ||
var add uintptr | ||
var addPtr = uintptr(unsafe.Pointer(&add)) | ||
``` | ||
|
||
Finally, the native function can be called: | ||
|
||
```go | ||
ret := goblend.Call(addPtr, 0) | ||
``` | ||
|
||
The `0` argument is the `ptr` to pass to the native function. You can pass and return any arbitrary data this way. | ||
|
||
### Callbacks | ||
|
||
TODO: Create example and document. |