Skip to content

Instantly share code, notes, and snippets.

@iamlouk
Created February 17, 2022 19:40
Show Gist options
  • Save iamlouk/b98065fcdac57a39849b97fd837062f8 to your computer and use it in GitHub Desktop.
Save iamlouk/b98065fcdac57a39849b97fd837062f8 to your computer and use it in GitHub Desktop.
Using cgo, libffi and reflection all at once makes calling functions from a dynamically loaded library in Go super easy!
package autoffi
/*
#cgo LDFLAGS: -lffi -ldl
#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>
#include <ffi.h>
#include <stdint.h>
*/
import "C"
import (
"errors"
"fmt"
"reflect"
"unsafe"
)
// Lib represents a dynamic library.
type Lib struct {
libname string
dlHandle unsafe.Pointer
}
// New calls C.dlopen and returns a new instance of a dynamic library.
func New(libname string) (*Lib, error) {
clibname := C.CString(libname)
defer C.free(unsafe.Pointer(clibname))
lib := &Lib{libname: libname}
lib.dlHandle = C.dlopen(clibname, C.int(C.RTLD_NOW|C.RTLD_GLOBAL))
if lib.dlHandle == C.NULL {
return nil, fmt.Errorf("dlopen: %s", C.GoString(C.dlerror()))
}
return lib, nil
}
// Destroy() calls C.dlclose(). Do not use any functions
// created by (*Lib).Func() anymore once called!
func (l *Lib) Destroy() error {
err := C.dlclose(l.dlHandle)
if err != 0 {
return fmt.Errorf("dlclose: %s", C.GoString(C.dlerror()))
}
return nil
}
// Given the name of a function symbol in this dynamic library,
// Func writes to fn a function that can be called. fn must be a
// pointer to a function type with value nil. The function
// is not allowed to have more than one return value and the only
// supported types (for now) are int8, int32 and int64.
func (l *Lib) Func(fnname string, fn interface{}) error {
cfnname := C.CString(fnname)
defer C.free(unsafe.Pointer(cfnname))
// Lookup in symbol table.
fnptr := C.dlsym(l.dlHandle, cfnname)
if fnptr == C.NULL {
return fmt.Errorf("dlsym: %s", C.GoString(C.dlerror()))
}
// Check that the second argument is a pointer to a function.
ptrValue := reflect.ValueOf(fn)
if ptrValue.Kind() != reflect.Ptr {
return errors.New("the second argument to Func() has to be a pointer to a nil function")
}
fnValue := ptrValue.Elem()
fnType := fnValue.Type()
if fnValue.Kind() != reflect.Func || !fnValue.IsNil() {
return errors.New("the second argument to Func() has to be a pointer to a nil function")
}
if fnType.IsVariadic() {
return errors.New("no variadic functions allowed")
}
var cif C.ffi_cif
// Prepare FFI call argument types
var cifArgTypes **C.ffi_type = (**C.ffi_type)(C.malloc(C.ulong(fnType.NumIn() * C.sizeof_uintptr_t))) // TODO: memory leak!
for i := 0; i < fnType.NumIn(); i++ {
argType := fnType.In(i)
pos := (**C.ffi_type)(unsafe.Add(unsafe.Pointer(cifArgTypes), i*C.sizeof_uintptr_t))
switch argType.Kind() {
case reflect.Int8:
*pos = &C.ffi_type_sint8
case reflect.Int32:
*pos = &C.ffi_type_sint32
case reflect.Int64:
*pos = &C.ffi_type_sint64
case reflect.Ptr:
*pos = &C.ffi_type_pointer
default:
return fmt.Errorf("argument type not allowed: %s", argType.Kind().String())
}
}
// Prepare FFI call return type
var cifRetType *C.ffi_type
if fnType.NumOut() == 0 {
cifRetType = &C.ffi_type_void
} else if fnType.NumOut() == 1 {
kind := fnType.Out(0).Kind()
switch kind {
case reflect.Int8:
cifRetType = &C.ffi_type_sint8
case reflect.Int32:
cifRetType = &C.ffi_type_sint32
case reflect.Int64:
cifRetType = &C.ffi_type_sint64
case reflect.Ptr:
cifRetType = &C.ffi_type_pointer
default:
return fmt.Errorf("return type not allowed: %s", kind.String())
}
} else {
return errors.New("only functions with zero or one return value are allowed")
}
// Initialize the cif structure.
status := C.ffi_prep_cif(&cif, C.FFI_DEFAULT_ABI, C.uint(fnType.NumIn()), cifRetType, cifArgTypes)
if status != C.FFI_OK {
return errors.New("ffi_prep_cif failed")
}
// Set the provided function pointer to a function that does the FFI call.
ptrValue.Elem().Set(reflect.MakeFunc(fnType, func(args []reflect.Value) []reflect.Value {
var result C.ffi_arg
// Prepare the array of pointers to arguments.
var cifVals *unsafe.Pointer = (*unsafe.Pointer)(C.malloc(C.ulong(fnType.NumIn()) * C.sizeof_uintptr_t))
defer C.free(unsafe.Pointer(cifVals))
for i, arg := range args {
// For whatever reason, arg is not addressable directly. Workaround:
ptr := reflect.New(arg.Type())
ptr.Elem().Set(arg)
pos := (*unsafe.Pointer)(unsafe.Add(unsafe.Pointer(cifVals), i*C.sizeof_uintptr_t))
*pos = unsafe.Pointer(ptr.Pointer())
}
// WOOOH! FFI...
C.ffi_call(&cif, (*[0]byte)(fnptr), unsafe.Pointer(&result), (*unsafe.Pointer)(cifVals))
if fnType.NumOut() == 0 {
return []reflect.Value{}
} else {
// Convert the return value to something useful
var val reflect.Value
switch fnType.Out(0).Kind() {
case reflect.Int8:
val = reflect.ValueOf(*(*int8)((unsafe.Pointer)(&result)))
case reflect.Int32:
val = reflect.ValueOf(*(*int32)((unsafe.Pointer)(&result)))
case reflect.Int64:
val = reflect.ValueOf(*(*int64)((unsafe.Pointer)(&result)))
case reflect.Ptr:
val = reflect.ValueOf(*(*unsafe.Pointer)((unsafe.Pointer)(&result)))
default:
panic("return type not allowed")
}
return []reflect.Value{val}
}
}))
return nil
}
package autoffi
import "testing"
func TestBasics(t *testing.T) {
lib, err := New("/usr/lib/libc.so.6")
if err != nil {
t.Fatal(err)
}
var putchar func(c int8) int32
if err := lib.Func("putchar", &putchar); err != nil {
t.Fatal(err)
}
putchar(int8('H'))
putchar(int8('i'))
putchar(int8('!'))
putchar(int8('\n'))
if err := lib.Destroy(); err != nil {
t.Fatal(err)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment