Created
February 17, 2022 19:40
-
-
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!
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
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 | |
} |
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
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