Skip to content

noctarius/libgoffi

Repository files navigation

libgoffi - idiomatic libffi library and arbitrary function adapter for Go

TLDR;

import (
  goffi "github.com/clevabit/libgoffi"
)

// Import libc library
library := goffi.NewLibrary("libc", goffi.BindNow|goffi.BindGlobal)

// Make a function definition matching the native function's signature
type getpid = func() int

// Create a Go variable of the function type
var fn getpid

// Import the getpid function and map it to the target variable
if err := library.Import("getpid", &fn); err != nil {
  // error handling
}

// Execute the function like it was a standard Go function
println(fmt.sprintf("pid: %d", fn()))

Introduction to libgoffi

libgoffi libgoffi?branch=master&svg=true Codacy code quality libgoffi?status libgoffi badge

libffi is one of the most commonly used libraries to implement foreign function interfaces in programming languages or frameworks.

While Go has an amazing built-in functionality to interoperate with C functions, the usage of the CGO pseudo interfaces is not very intuitive. Especially in combination with dynamic libraries (dlopen, dlsym, dlclose) the complexity increases exponentially, due to the of loading and unloading the library and manually determining the symbol address.

libgoffi implements a wrapper library to automatically load dynamic libraries into the applicationÄs memory space (using go-dl by Achille Roussel), and map imported functions onto their Go function type counterparts, using libffi’s common foreign function interface.

The necessary functions are generated at runtime by utilizing Go’s reflection library and setting up the types and stub according to the given function signatures.

Attention: Generating the code statically may be possible, but is not implemented at time of writing.

libgoffi automatically maps the most commonly used data types between Go and C bi-directionally.

Attention: libgoffi may run with Go compiler versions lower than 1.12, but this is not tested. Pull requests to support older and newer versions are welcome though.

Supported Data Types

libgoffi currently supports mapping of the most commonly used Go data types. Mapping can be applied manually, by specifying both parameter signatures, or automaticly by providing the Go function signature when importing the function, and have libgoffi figure out the according C function signature based on the table below.

Mapping for the following data types is implemented:

Table 1. Data Type Mapping
Go Data Type C Data Type libffi Data Type

uint

uint16_t / uint32_t

ffi_type_uint16 / ffi_type_uint32

uint8

uint8_t

ffi_type_uint8

uint16

uint16_t

ffi_type_uint16

uint32

uint32_t

ffi_type_uint32

uint64

uint64_t

ffi_type_uint64

int8

int8_t

ffi_type_sint8

int16

int16_t

ffi_type_sint16

int32

int32_t

ffi_type_sint32

int64

int64_t

ffi_type_sint64

float32

float_t

ffi_type_float

float64

double_t

ffi_type_double

unsafe.Pointer

void *

ffi_type_pointer

uintptr

void *

ffi_type_pointer

safe pointers (&var)

void *

ffi_type_pointer

-

void

ffi_type_void

Attention: The Go types uint and int mapping is platform specific and determined by looking at the maximum value of a signed int in C. Please be aware when automatically mapping those data types. It is advised to use more specific data types, such as int32 or uint32 to be platform independent.

Attention: Mapping of structs is not supported. It is recommended to create all pointers to structs, that need to be passed to C code, by allocating them using C.malloc(…). This prevents the Go runtime to throw the "Go Pointer to Go Pointer" exception.

Attention: When passing a Go String to a function, remember, that it is mapped to a char * data type in C. That means, the string will be extended by adding 0x00 byte (a zero byte, \0) at the end. Therefore, if the function in questions requires passing the length of the string, the actual string passed is one byte longer than the string in Go (len(msg)+1).

Attention: Returning a char * from a function in C (which is uncommon), libgoffi will normally map it to a string in Go. Furthermore, libgoffi will immediately free the actual C pointer returned from the function. If this behavior is unintentional, the mapping must specifically ask to return the char * itself. The transformation into a Go String (using C.GoString(ptr)) must be done manually, as freeing of the pointer.

Supported Operating Systems

lobgoffi should support almost every posix compliant OS, however, only code for Linux and OSX (Darwin) has been tested.

A few lines of code should make it possible to support operating systems like FreeBSD or similar. Pull requests to add support for other operating systems are welcome.

Windows is not supported, since Windows does not support dlopen, dlsym and dlclose. It should be totally possible to support the WinAPI functions to load .DLL files though. Again pull requests adding support for Windows are welcome.

Not Supported

  • lobgoffi does not support signature checking. When executing an imported function using a wrong function signature, your program may just return a wrong value. In the worst case, however, it may actually crash with a segmentation fault or another exception of any kind. In any case, it will not behave as expected (or does it? :-)).

  • As mentioned above, Windows is also not supported, as are any other posix compliant operating systems other than Linux and OSX (Darwin). In theory any posix OS supported by both Go and libffi should be possible to support though.

  • Mapping of Go structs is not supported and not the subject of this library adapter. Pointers to Go memory are always complicated to handle, and error prone. More information on CGO interaction and Go pointers can be found in the official Go documentation.

  • Last but not least, function pointers are not officially supported or tested, but may work when exported as C functions. This might be officially supported in the future though.

Loading a Library

Thanks to the go-dl library which is used to load the underlying dynamic library, libgoffi tries hard to automatically determine the actual path of a library.

Loading a library is normally as easy as asking by its name:

import (
  goffi "github.com/clevabit/libgoffi"
)

library := goffi.NewLibrary("libc", goffi.BindNow|goffi.BindGlobal)

libgoffi provides some binding flags of the posix API, more specifically:

  • BindLazy

  • BindNow

  • BindLocal

  • BindGlobal

The binding flags are XOR’ed together before being passed to the loader.

More information on those flags can be found in the Linux manpages.

Import Functions

Importing functions from the loaded library is provided using 3 different styles, depending on how much type mapping is necessary and how complex function types are designed.

Fully Automatic Data Type Mapping

libgoffi is able to provide a fully automatic type mapping, which is probably enough to map the most common functions.

The following example expects the libc library to already being loaded into the application as shown in the previous section.

// Make a function definition matching the native function's signature
type getpid = func() int

// Create a Go variable of the function type
var fn getpid

// Import the getpid function and map it to the target variable
if err := library.Import("getpid", &fn); err != nil {
  // error handling
}

// Execute the function like it was a standard Go function
println(fmt.sprintf("pid: %d", fn()))

In this example we imported the getpid function from libc, which in itself returns the pid (process identifier) of the currently running application, that said, our demo application.

This mapping type also works for functions that expect one or more parameters.

type sqrt = func(float64) float64

var fn sqrt
if err := library.Import("sqrt", &fn); err != nil {
  // error handling
}
println(fmt.sprintf("sqrt of 9.0: %f", fn(9.)))

It is also always possible to map out error return types as the last parameter of the function definition. The error will not be mapped out to the C function signature, but used by the library to report errors during execution of the function, like illegal parameter values.

An example of such a function mapping would be (using the sqrt example again):

type sqrt = func(float64) (float64, error)

var fn sqrt
if err := library.Import("sqrt", &fn); err != nil {
  // error handling
}
sq, err := fn(9.)
if err != nil {
  // error handling
}
println(fmt.sprintf("sqrt of 9.0: %f", sq))

If no explicit error handling is requested as part of the function’s signature, libgoffi will use panics to report the malfunctioning behavior. It is advised to explicitly map errors are return parameters to prevent unexpected panics.

New Function Import

In addition to mapping a C function to an existing variable of a specific Go function type, libgoffi can also create function mappers for freely defined (reflective) function definitions.

For example we can import both of the above functions again, but this time using the explicit factory function.

// Create a new function which returns an int and an error (the third parameter)
fn, err := library.NewImport("getpid", goffi.TypeInt, true)
if err != nil {
  // error handling
}

// Type assertion to the specific Go function type
getpid, ok := fn.(func()(int, error))
if !ok {
  // error handling
}

// Execute the function like it was a standard Go function
println(fmt.sprintf("pid: %d", getpid()))

In this example we mapped the getpid function again and told the mapper we also want to report errors back. Remember, not reporting errors may result in a runtime panic in case of any issues with the mapping.

To map the returned function to a callable variable, a type assertions is required. Type assertions provide the benefit of automatic runtime type checking.

For the next example we import the sqrt function again, but this time we will not map out errors though (third parameter is false). However, we also provide the parameter signature (a single float64 parameter). The parameter signature is a variadic argument and can take an arbitrary number of type arguments.

fn, err := library.NewImport("sqrt", goffi.TypeFloat64, false, goffi.TypeFloat64)
if err != nil {
  // error handling
}

sqrt, ok := fn.(func(float64) float64)
if !ok {
  // error handling
}

println(fmt.sprintf("sqrt of 9.0: %f", sqrt(9.)))

Complex Data Type Mapping

Sometimes, a more complex type mapping is necessary. This is especially the case, when the there is no automatic mapping for a library specific C data type.

libgoffi provides a specific import function for complex use cases. It is able to be provided with a specific set of Go and C side function type definitions.

libgoffi will try its best to map the given C type to the Go type, and vise versa. It can, for example, be used to map number types in C or Go to another data type in the other language. Complex mapping of numbers can be dangerous though and types may be incorrectly be narrowed or widened if erroneously specificied. Also be aware of potential overflow handling when mapping between unsigned and signed data types.

To show a more complex example, we will be passing an int to the sqrt function, even though the C function clearly expects a float parameter. We also ask to return an int. The return value translation is possible but will truncate the data to an integer representation.

The type translation is automatically handled by libgoffi before passing the value onwards to the imported function.

It is possible to just translate the parameter or return type, too.

// Define the Go function signature
fnGo := reflect.FuncOf(
  []reflect.Type{goffi.TypeInt},     // input types
  []reflect.Type{goffi.TypeInt},     // output types
  false,                             // non-variadic
)

// Define the C function signature
fnC := reflect.FuncOf(
  []reflect.Type{goffi.TypeFloat64}, // input types
  []reflect.Type{goffi.TypeFloat64}, // output types
  false,                             // non-variadic
)

// Import the function
fn, err := library.NewImportComplex("sqrt", fnGo, fnC)
if err != nil {
  // error handling
}

sqrt, ok := fn.(func(int) int)
if !ok {
  // error handling
}

println(fmt.sprintf("sqrt of 9: %d", sqrt(9)))

Closing a Loaded Library

libgoffi uses internal caches to store state and loaded symbols. Furthermore, it also allocates memory outside of the Go heap. That said, a loaded library should be closed explicitly to free allocated resources.

A simple call to the Close() function is enough.

if err := library.Close(); err != nil {
  // error handling
}

Why libgoffi?

libgoffi provides Go idiomatic loading, importing and mapping of C functions, without the complexity of manual handling the CGO, when dealing with dynamic libraries.

While Go and CGO provide a good solution to support statically linked libraries, depending on the use case, linking all libraries statically may not be the preferred solution.

Especially in embedded environments space is limitted and a library already on disk (and used by other tools) doesn’t need to be duplicated with static linking.

Loading a dynamic library, importing symbols and mapping calls, however, can be a tedious job. That’s why lobgoffi hides the complexity, and provides a clean and idiomatic Go interface.

License

libgoffi is provided under the Apache License 2.0. That means, it can freely be copied, used, updated, changed. Code changes do not need to be upstreamed back to the project, we’d love however to see users to provide additional functionality, mappings or just bugfixes or feature requests and ideas.

Liability

libgoffi is provided by the clevabit GmbH for free and as is. clevabit is not liable for any damage on software, hardware, or of any other nature, which is related to the usage of this library.

About

libgoffi - libffi adapter library for Go

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published