purego

package module
v0.7.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 4, 2024 License: Apache-2.0 Imports: 9 Imported by: 70

README

purego

Go Reference

A library for calling C functions from Go without Cgo.

This is beta software so expect bugs and potentially API breaking changes but each release will be tagged to avoid breaking people's code. Bug reports are encouraged.

Motivation

The Ebitengine game engine was ported to use only Go on Windows. This enabled cross-compiling to Windows from any other operating system simply by setting GOOS=windows. The purego project was born to bring that same vision to the other platforms supported by Ebitengine.

Benefits

  • Simple Cross-Compilation: No C means you can build for other platforms easily without a C compiler.
  • Faster Compilation: Efficiently cache your entirely Go builds.
  • Smaller Binaries: Using Cgo generates a C wrapper function for each C function called. Purego doesn't!
  • Dynamic Linking: Load symbols at runtime and use it as a plugin system.
  • Foreign Function Interface: Call into other languages that are compiled into shared objects.
  • Cgo Fallback: Works even with CGO_ENABLED=1 so incremental porting is possible. This also means unsupported GOARCHs (freebsd/riscv64, linux/mips, etc.) will still work except for float arguments and return values.

Supported Platforms

  • FreeBSD: amd64, arm64
  • Linux: amd64, arm64
  • macOS / iOS: amd64, arm64
  • Windows: 386*, amd64, arm*, arm64

* These architectures only support SyscallN and NewCallback

Example

This example only works on macOS and Linux. For a complete example look at libc which supports Windows and FreeBSD.

package main

import (
	"fmt"
	"runtime"

	"github.com/ebitengine/purego"
)

func getSystemLibrary() string {
	switch runtime.GOOS {
	case "darwin":
		return "/usr/lib/libSystem.B.dylib"
	case "linux":
		return "libc.so.6"
	default:
		panic(fmt.Errorf("GOOS=%s is not supported", runtime.GOOS))
	}
}

func main() {
	libc, err := purego.Dlopen(getSystemLibrary(), purego.RTLD_NOW|purego.RTLD_GLOBAL)
	if err != nil {
		panic(err)
	}
	var puts func(string)
	purego.RegisterLibFunc(&puts, libc, "puts")
	puts("Calling C from Go without Cgo!")
}

Then to run: CGO_ENABLED=0 go run main.go

Questions

If you have questions about how to incorporate purego in your project or want to discuss how it works join the Discord!

External Code

Purego uses code that originates from the Go runtime. These files are under the BSD-3 License that can be found in the Go Source. This is a list of the copied files:

  • abi_*.h from package runtime/cgo
  • zcallback_darwin_*.s from package runtime
  • internal/fakecgo/abi_*.h from package runtime/cgo
  • internal/fakecgo/asm_GOARCH.s from package runtime/cgo
  • internal/fakecgo/callbacks.go from package runtime/cgo
  • internal/fakecgo/go_GOOS_GOARCH.go from package runtime/cgo
  • internal/fakecgo/iscgo.go from package runtime/cgo
  • internal/fakecgo/setenv.go from package runtime/cgo
  • internal/fakecgo/freebsd.go from package runtime/cgo

The files abi_*.h and internal/fakecgo/abi_*.h are the same because Bazel does not support cross-package use of #include so we need each one once per package. (cf. issue)

Documentation

Index

Examples

Constants

View Source
const (
	RTLD_DEFAULT = 0x00000 // Pseudo-handle for dlsym so search for any loaded symbol
	RTLD_LAZY    = 0x00001 // Relocations are performed at an implementation-dependent time.
	RTLD_NOW     = 0x00002 // Relocations are performed when the object is loaded.
	RTLD_LOCAL   = 0x00000 // All symbols are not made available for relocation processing by other modules.
	RTLD_GLOBAL  = 0x00100 // All symbols are available for relocation processing of other modules.
)

Variables

This section is empty.

Functions

func Dlclose

func Dlclose(handle uintptr) error

Dlclose decrements the reference count on the dynamic library handle. If the reference count drops to zero and no other loaded libraries use symbols in it, then the dynamic library is unloaded.

func Dlopen

func Dlopen(path string, mode int) (uintptr, error)

Dlopen examines the dynamic library or bundle file specified by path. If the file is compatible with the current process and has not already been loaded into the current process, it is loaded and linked. After being linked, if it contains any initializer functions, they are called, before Dlopen returns. It returns a handle that can be used with Dlsym and Dlclose. A second call to Dlopen with the same path will return the same handle, but the internal reference count for the handle will be incremented. Therefore, all Dlopen calls should be balanced with a Dlclose call.

func Dlsym

func Dlsym(handle uintptr, name string) (uintptr, error)

Dlsym takes a "handle" of a dynamic library returned by Dlopen and the symbol name. It returns the address where that symbol is loaded into memory. If the symbol is not found, in the specified library or any of the libraries that were automatically loaded by Dlopen when that library was loaded, Dlsym returns zero.

func NewCallback

func NewCallback(fn interface{}) uintptr

NewCallback converts a Go function to a function pointer conforming to the C calling convention. This is useful when interoperating with C code requiring callbacks. The argument is expected to be a function with zero or one uintptr-sized result. The function must not have arguments with size larger than the size of uintptr. Only a limited number of callbacks may be created in a single Go process, and any memory allocated for these callbacks is never released. At least 2000 callbacks can always be created. Although this function provides similar functionality to windows.NewCallback it is distinct.

Example
package main

import (
	"fmt"

	"github.com/ebitengine/purego"
)

func main() {
	cb := purego.NewCallback(func(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 int) int {
		fmt.Println(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15)
		return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 + a11 + a12 + a13 + a14 + a15
	})

	var fn func(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 int) int
	purego.RegisterFunc(&fn, cb)

	ret := fn(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
	fmt.Println(ret)

}
Output:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
120

func RegisterFunc

func RegisterFunc(fptr interface{}, cfn uintptr)

RegisterFunc takes a pointer to a Go function representing the calling convention of the C function. fptr will be set to a function that when called will call the C function given by cfn with the parameters passed in the correct registers and stack.

A panic is produced if the type is not a function pointer or if the function returns more than 1 value.

These conversions describe how a Go type in the fptr will be used to call the C function. It is important to note that there is no way to verify that fptr matches the C function. This also holds true for struct types where the padding needs to be ensured to match that of C; RegisterFunc does not verify this.

Type Conversions (Go <=> C)

string <=> char*
bool <=> _Bool
uintptr <=> uintptr_t
uint <=> uint32_t or uint64_t
uint8 <=> uint8_t
uint16 <=> uint16_t
uint32 <=> uint32_t
uint64 <=> uint64_t
int <=> int32_t or int64_t
int8 <=> int8_t
int16 <=> int16_t
int32 <=> int32_t
int64 <=> int64_t
float32 <=> float
float64 <=> double
struct <=> struct (WIP - darwin only)
func <=> C function
unsafe.Pointer, *T <=> void*
[]T => void*

There is a special case when the last argument of fptr is a variadic interface (or []interface} it will be expanded into a call to the C function as if it had the arguments in that slice. This means that using arg ...interface{} is like a cast to the function with the arguments inside arg. This is not the same as C variadic.

Memory

In general it is not possible for purego to guarantee the lifetimes of objects returned or received from calling functions using RegisterFunc. For arguments to a C function it is important that the C function doesn't hold onto a reference to Go memory. This is the same as the Cgo rules.

However, there are some special cases. When passing a string as an argument if the string does not end in a null terminated byte (\x00) then the string will be copied into memory maintained by purego. The memory is only valid for that specific call. Therefore, if the C code keeps a reference to that string it may become invalid at some undefined time. However, if the string does already contain a null-terminated byte then no copy is done. It is then the responsibility of the caller to ensure the string stays alive as long as it's needed in C memory. This can be done using runtime.KeepAlive or allocating the string in C memory using malloc. When a C function returns a null-terminated pointer to char a Go string can be used. Purego will allocate a new string in Go memory and copy the data over. This string will be garbage collected whenever Go decides it's no longer referenced. This C created string will not be freed by purego. If the pointer to char is not null-terminated or must continue to point to C memory (because it's a buffer for example) then use a pointer to byte and then convert that to a slice using unsafe.Slice. Doing this means that it becomes the responsibility of the caller to care about the lifetime of the pointer

Structs

Purego can handle the most common structs that have fields of builtin types like int8, uint16, float32, etc. However, it does not support aligning fields properly. It is therefore the responsibility of the caller to ensure that all padding is added to the Go struct to match the C one. See `BoolStructFn` in struct_test.go for an example.

Example

All functions below call this C function:

char *foo(char *str);

// Let purego convert types
var foo func(s string) string
goString := foo("copied")
// Go will garbage collect this string

// Manually, handle allocations
var foo2 func(b string) *byte
mustFree := foo2("not copied\x00")
defer free(mustFree)

func RegisterLibFunc

func RegisterLibFunc(fptr interface{}, handle uintptr, name string)

RegisterLibFunc is a wrapper around RegisterFunc that uses the C function returned from Dlsym(handle, name). It panics if it can't find the name symbol.

func SyscallN

func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr)

SyscallN takes fn, a C function pointer and a list of arguments as uintptr. There is an internal maximum number of arguments that SyscallN can take. It panics when the maximum is exceeded. It returns the result and the libc error code if there is one.

NOTE: SyscallN does not properly call functions that have both integer and float parameters. See discussion comment https://github.com/ebiten/purego/pull/1#issuecomment-1128057607 for an explanation of why that is.

On amd64, if there are more than 8 floats the 9th and so on will be placed incorrectly on the stack.

The pragma go:nosplit is not needed at this function declaration because it uses go:uintptrescapes which forces all the objects that the uintptrs point to onto the heap where a stack split won't affect their memory location.

Types

type Dlerror

type Dlerror struct {
	// contains filtered or unexported fields
}

Dlerror represents an error value returned from Dlopen, Dlsym, or Dlclose.

func (Dlerror) Error added in v0.3.0

func (e Dlerror) Error() string

Directories

Path Synopsis
examples
internal
cgo
Package objc is a low-level pure Go objective-c runtime.
Package objc is a low-level pure Go objective-c runtime.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL