Giter VIP home page Giter VIP logo

nocgo's Introduction

nocgo

Tested on go1.11 and go1.12.

GoDoc

This repository/package contains a proof of concept for calling into C code without using cgo.

WARNING! This is meant as a proof of concept and subject to changes. Furthermore this is highly experimental code. DO NOT USE IN PRODUCTION. This could cause lots of issues from random crashes (there are tests - but there is definitely stuff that's not tested) to teaching your gopher to talk C gibberish.

WARNING nocgo supports both cgo and missing cgo as environment. So if you want to ensure cgo not being used don't forget CGO_ENABLED=0 as environment variable to go build.

Todo

  • Callbacks into go
  • Structures

When that's done write up a proposal for golang inclusion.

Usage

Libraries can be loaded and unloaded similar to dlopen and dlclose, but acquiring symbols (i.e., functions, global variables) is a bit different, since a function specification (i.e., arguments, types, return type) is also needed. Furthermore, C-types must be translated to go-types and vice versa.

This works by providing a function specification as a pointer to a function variable. A call to lib.Func will examine arguments and eventual return value (only one or no return values allowed!), and set the function variable to a wrapper that will call into the desired C-function.

Type Mappings

Go types will be mapped to C-types according to the following table:

Go type C Type
int8, byte char
uint8, bool unsigned char
int16 short
uint16 unsigned short
int32 int
uint32 unsigned int
int64 long
uint64 unsigned long
float32 float
float64 double
[], uintptr, reflect.UnsafePointer, * *

The last line means that slices and pointers are mapped to pointers in C. Pointers to structs are possible.

Passing struct, complex, and callback functions is not (yet) supported.

WARNING structs that are referenced must follow C alignment rules! There is no type checking, since this is actually not possible due to libraries not knowing their types...

Go int was deliberately left out to avoid confusion, since it has different sizes on different architectures.

Example

An example using pcap_open_live from libpcap (C-definition: pcap_t *pcap_open_live(const char *device, int snaplen, int promisc, int to_ms, char *errbuf) ) could look like the following example:

// Load the library
lib, err := nocgo.Open("libpcap.so")
if err != nil {
    log.Fatalln("Couldn't load libpcap: ", err)
}

// func specification
var pcapOpenLive func(device []byte, snaplen int32, promisc int32, toMS int32, errbuf []byte) uintptr
// Get a handle for the function
if err := lib.Func("pcap_open_live", &pcapOpenLive); err != nil {
    log.Fatalln("Couldn't get pcap_open_live: ", err)
}

// Do the function call
errbuf := make([]byte, 512)
pcapHandle := pcapOpenLive(nocgo.MakeCString("lo"), 1500, 1, 100, errbuf)

// Check return value
if pcapHandle == 0 {
    log.Fatalf("Couldn't open %s: %s\n", "lo", nocgo.MakeGoStringFromSlice(errbuf))
}

// pcapHandle can now be used as argument to the other libpcap functions

A full example is contained in examplelibpcap and another one in example.

WARNING nocgo supports both cgo and missing cgo as environment. So if you want to ensure cgo not being used don't forget CGO_ENABLED=0 as environment variable to go build.

Supported Systems

  • linux with glibc
  • FreeBSD
    Errata: FreeBSD requires the exported symbols _environ and _progname. This is only possible inside cgo or stdlib. So for building on FreeBSD, -gcflags=github.com/notti/nocgo/fakecgo=-std is required (This doesn't seem to work for go test - so examples work, but test does not)).

With some small modifications probably all systems providing dlopen can be supported. Have a look at dlopen_OS.go and symbols_OS.go in fakecgo.

Supported Architectures

  • 386
  • amd64

Implementing further architectures requires

  • Building trampolines for fakecgo (see below)
  • Implementing the cdecl callspec in call_.go/.s

How does this work

nocgo

nocgo imports dlopen, dlclose, dlerror, dlsym via go:cgo_import_dynamic in dlopen_OS.go. lib.Func builds a specification on where to put which argument in call_arch.go. go calls such a function by dereferencing, where it points to, provide this address in a register and call the first address that is stored there. nocgo uses this mechanism by putting a struct there, that contains the address to a wrapper followed by a pointer to the what dlsym provided and a calling specification. The provided wrapper uses cgocall from the runtime to call an assembly function and pass the spec and a pointer to the arguments to it. This assembly function is implemented in call_arch.s and it uses the specification to place the arguments into the right places, calls the pointer provided by dlsym and then puts the return argument into the right place if needed.

This is basically what libffi does. So far cdecl for 386 (pass arguments on the stack in right to left order, return values are in AX/CX or ST0) and amd64 (pass arguments in registers DI, SI, DX, CX, R8, R9/X0-X7 and the stack in right to left order, number of floats in AX, fixup alignment of stack) are implemented.

So far so simple. cgocall could actually be used to call a C function directly - but it is only capable of providing one argument!

But there is a second issue. For simple C functions we could leave it at that (well we would need to use asmcgocall, because cgocall checks, if cgo is actually there...). But there is this thing called Thread Local Storage (TLS) that is not too happy about golang not setting that up correctly. This is already needed if you do printf("%f", 1) with glibc!

So we need to provide some functionality that cgo normally provides, which is implemented in fakecgo:

fakecgo

go sets up it's own TLS during startup in runtime/asm_arch.s in runtime·rt0_go. We can easily prevent that by providing setting the global variable _cgo_init to something non-zero (easily achieved with go:linkname and setting a value). But this would crash go, since if this is the case, go actually calls the address inside this variable (well ok we can provide an empty function).

Additionally, this would provide correct TLS only on the main thread. This works until one does a lot more than just call one function, so we need to fixup also some other stuff.

So next step: set runtime.is_cgo to true (again - linkname to the rescue). But this will panic since now the runtime expects the global variables _cgo_thread_start, _cgo_notify_runtime_init_done, _cgo_setenv, and _cgo_unsetenv to point to something. Ok so let's just implement those.

  • _cgo_notify_runtime_init_done is easy - we don't need this one: empty function.
  • _cgo_setenv is also simple: just one function call to setenv
  • _cgo_unsetenv is the same.
  • _cgo_init queries the needed stack size to update g->stack so that runtime stack checks do the right thing (it also provides a setg function we come to that later...)
  • _cgo_thread_start is a bit more involved... It starts up a new thread with pthread_create and does a bit of setup.

So this should be doable - right?

Well easier said than done - those are implemented in C-code in runtime/cgo/*c presenting some kind of chicken and egg problem to us.

So I started out with reimplementing those in go assembly (remember: we want to be cgo free) which is available in the tag asm. Since this is really cumbersome and needs a lot of code duplication, I experimented a bit if we can do better.

Aaaand we can:

fakecgo/trampoline_arch.s contains the above mentioned entry points, and "converts" the C-calling conventions to go calling conventions (e.g. move register passed arguments to the stack). Then it calls the go functions in fakecgo/cgo.go.

Ok - but we still need all those pthread and C-library-functions. Well we can import the symbols (like with dlopen). So all we need is a way to call those:

The trampoline file also contains an asmlibccall6 function that can call C-functions with a maximum of 6 integer arguments and one return value. fakecgo/libccall.go maps this onto more convenient go functions with 1-6 arguments and fakecgo/libcdefs.go further maps those into nice functions that look like the C functions (e.g. func pthread_create(thread *pthread_t, attr *pthread_attr, start, arg unsafe.Pointer) int32). Well this was not exactly my idea - the runtime already does that for solaris and darwin (runtime/os_solaris.go, runtime/syscall_solaris.go, runtime/sys_solaris_amd64.s) - although my implementation here is kept a bit simpler since it only ever will be called from gocode pretending to be C.

So now we can implement all the above mentioned cgo functions in pure (but sometimes a bit ugly) go in fakecgo/cgo.go. Ugly, because those functions are called with lots of functionality missing! Writebarriers are not allowed, as are stack splits.

The upside is, that the only arch dependent stuff are the trampolines (in assembly) and the only OS dependent stuff are the symbol imports.

Except for freebsd (which needs two exported symbols, as mentioned above) all those things work outside the runtime and no special treatment is needed. Just import fakecgo and all the cgo setup just works (except if you use cgo at the same time - then the linker will complain).

Benchmarks

This will be a bit slower than cgo. Most of this is caused by argument rearranging:

386

name           old time/op    new time/op    delta
Empty-4          84.5ns ± 0%    86.4ns ± 2%    +2.22%  (p=0.000 n=8+8)
Float2-4         87.9ns ± 1%   222.5ns ± 6%  +153.20%  (p=0.000 n=8+10)
StackSpill3-4     116ns ± 1%     130ns ± 1%   +12.04%  (p=0.000 n=8+8)

Float is so slow since that type is at the end of the comparison chain.

amd64

name           old time/op    new time/op    delta
Empty-4          76.8ns ±10%    80.1ns ± 9%   +4.24%  (p=0.041 n=10+10)
Float2-4         78.4ns ± 5%    81.4ns ± 9%   +3.80%  (p=0.033 n=9+10)
StackSpill3-4    96.2ns ± 5%   120.7ns ± 7%  +25.46%  (p=0.000 n=10+9)

nocgo's People

Contributors

notti avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

nocgo's Issues

/usr/lib/x86_64-linux-gnu/libpthread.so: invalid ELF header

Hello @notti ,

I'm trying to test this against my SQLite3 bindings, which I've modified to add Linux support with this library and pushed to a temporary repository: https://github.com/iamacarpet/go-sqlite3-dynamic

Trying to run it, I created a new folder with main_test.go, running with CGO_ENABLED=0 go test

package sqlite3

import (
	"os"
	"testing"

	"database/sql"

	_ "github.com/iamacarpet/go-sqlite3-dynamic"

	"github.com/stretchr/testify/assert"
)

func TestXxx(t *testing.T) {
	path := "test.db"

	f, err := os.Create(path)
	if err != nil {
		t.Fatal(err)
	}
	f.Close()

	db, err := sql.Open(`sqlite3`, path)
	if err != nil {
		t.Fatal(err)
	}

	r, err := db.Exec(`CREATE TABLE test (
		id integer PRIMARY KEY NOT NULL,
		name varchar(30)
	)`)
	if err != nil {
		t.Fatal(err)
	}

	_ = r

	r, err = db.Exec(`INSERT INTO test(name) VALUES ('first') `)
	if err != nil {
		t.Fatal(err)
	}
	rowid, err := r.LastInsertId()
	if err != nil {
		t.Fatal(err)
	}
	affected, err := r.RowsAffected()
	if err != nil {
		t.Fatal(err)
	}
	assert.Equal(t, int64(1), rowid)
	assert.Equal(t, int64(1), affected)

	r, err = db.Exec(`INSERT INTO test(name) VALUES ('second') `)
	if err != nil {
		t.Fatal(err)
	}
	rowid, err = r.LastInsertId()
	if err != nil {
		t.Fatal(err)
	}
	affected, err = r.RowsAffected()
	if err != nil {
		t.Fatal(err)
	}
	assert.Equal(t, int64(2), rowid)
	assert.Equal(t, int64(1), affected)

	db.Close()
	os.Remove(`./test.db`)
}

It's throwing the error:

/tmp/go-build846919307/b001/sqlite3-test.test: error while loading shared libraries: /usr/lib/x86_64-linux-gnu/libpthread.so: invalid ELF header
exit status 127
FAIL    sqlite3-test    0.001s

On Ubuntu 18.04, /usr/lib/x86_64-linux-gnu/libpthread.so is an text file:

$ file /usr/lib/x86_64-linux-gnu/libpthread.so
/usr/lib/x86_64-linux-gnu/libpthread.so: ASCII text
$ cat /usr/lib/x86_64-linux-gnu/libpthread.so
/* GNU ld script
   Use the shared library, but some functions are only in
   the static library, so try that secondarily.  */
OUTPUT_FORMAT(elf64-x86-64)
GROUP ( /lib/x86_64-linux-gnu/libpthread.so.0 /usr/lib/x86_64-linux-gnu/libpthread_nonshared.a )

I checked for references to libpthread.so in your code and found a few.

Looking here , changing libpthread.so to libpthread.so.0 did nothing.

When I changed all occurrences in this file, the error changes to a segfault.

$ CGO_ENABLED=0 go test
fatal error: unexpected signal during runtime execution
[signal SIGSEGV: segmentation violation code=0x2 addr=0x7efe3f722fa8 pc=0x7efe3f50be19]

runtime stack:
runtime.throw(0x746fc8, 0x2a)
        /usr/local/go/src/runtime/panic.go:617 +0x72
runtime.sigpanic()
        /usr/local/go/src/runtime/signal_unix.go:374 +0x4a9

goroutine 1 [syscall, locked to thread]:
runtime.cgocall(0x522090, 0xc0000a7d88, 0xc0000a7da8)
        /usr/local/go/src/runtime/cgocall.go:128 +0x5b fp=0xc0000a7d78 sp=0xc0000a7d40 pc=0x40418b
github.com/notti/nocgo.callWrapper(0xc000084060, 0x2a, 0x30, 0xc000000002, 0x29, 0x30, 0x7efe3f6d46d0, 0x0, 0x40b4ed, 0xc0000a7f28, ...)
        ~/go-projects/src/github.com/notti/nocgo/call_amd64.s:69 +0x49 fp=0xc0000a7da8 sp=0xc0000a7d78 pc=0x522069
github.com/notti/nocgo.Open(0x746546, 0x29, 0x40c248, 0x70, 0x71bdc0)
        ~/go-projects/src/github.com/notti/nocgo/dlopen.go:61 +0x84 fp=0xc0000a7e00 sp=0xc0000a7da8 pc=0x521114
github.com/iamacarpet/go-sqlite3-dynamic.registerLibrary()
        ~/go-projects/src/github.com/iamacarpet/go-sqlite3-dynamic/dynamic_register_linux.go:65 +0x4b fp=0xc0000a7f38 sp=0xc0000a7e00 pc=0x527bcb
github.com/iamacarpet/go-sqlite3-dynamic.init.0()
        ~/go-projects/src/github.com/iamacarpet/go-sqlite3-dynamic/sqlite3.go:23 +0x22 fp=0xc0000a7f68 sp=0xc0000a7f38 pc=0x5290e2
github.com/iamacarpet/go-sqlite3-dynamic.init()
        <autogenerated>:1 +0x93 fp=0xc0000a7f78 sp=0xc0000a7f68 pc=0x52e863
sqlite3-test.init()
        <autogenerated>:1 +0x54 fp=0xc0000a7f88 sp=0xc0000a7f78 pc=0x695954
main.init()
        <autogenerated>:1 +0x54 fp=0xc0000a7f98 sp=0xc0000a7f88 pc=0x695bf4
runtime.main()
        /usr/local/go/src/runtime/proc.go:188 +0x1c8 fp=0xc0000a7fe0 sp=0xc0000a7f98 pc=0x42ea08
runtime.goexit()
        /usr/local/go/src/runtime/asm_amd64.s:1337 +0x1 fp=0xc0000a7fe8 sp=0xc0000a7fe0 pc=0x45b481
exit status 2
FAIL    sqlite3-test    0.009s

My Go version:

$ go version
go version go1.12.4 linux/amd64

Am I doing something stupid?

Regards,
iamacarpet

LICENSE file?

Hey, this is an incredible piece of work, but I would feel much better about using it with a license file.

Any chance of adding a license?

If you are looking for suggestions, I would say consider MIT License.

Thanks for this great way of calling .so files without cgo.

Cameron

cgocall unavailable without CGO_ENABLED=0

Hello @notti

Just finishing up my documentation and I noticed in yours it says:

WARNING nocgo supports both cgo and missing cgo as environment. So if you want to ensure cgo not being used don't forget CGO_ENABLED=0 as environment variable to go build.

However, building without CGO_ENABLED=0 always throws me this error:

fatal error: cgocall unavailable

goroutine 1 [running, locked to thread]:
runtime.throw(0x4f53b4, 0x13)
        /usr/local/go/src/runtime/panic.go:617 +0x72 fp=0xc000090d50 sp=0xc000090d20 pc=0x429d02
runtime.cgocall(0x4a8ed0, 0xc000090d98, 0xc000090db8)
        /usr/local/go/src/runtime/cgocall.go:96 +0xe4 fp=0xc000090d88 sp=0xc000090d50 pc=0x403d34
github.com/notti/nocgo.callWrapper(0xc000086010, 0x10, 0x10, 0xc000000002, 0xf, 0x10, 0x7fbff35f9d98, 0x0, 0xaa, 0xc000090f38, ...)
        ~/go-projects/src/github.com/notti/nocgo/call_amd64.s:69 +0x49 fp=0xc000090db8 sp=0xc000090d88 pc=0x4a8ea9
github.com/notti/nocgo.Open(0x4f47d3, 0xf, 0x40b768, 0x70, 0x4e6f60)
        ~/go-projects/src/github.com/notti/nocgo/dlopen.go:61 +0x84 fp=0xc000090e10 sp=0xc000090db8 pc=0x4a7f54
github.com/iamacarpet/go-sqlite3-dynamic.registerLibrary()
        ~/go-projects/src/github.com/iamacarpet/go-sqlite3-dynamic/dynamic_register_linux.go:65 +0x4b fp=0xc000090f48 sp=0xc000090e10 pc=0x4ac4cb
github.com/iamacarpet/go-sqlite3-dynamic.init.0()
        ~/go-projects/src/github.com/iamacarpet/go-sqlite3-dynamic/sqlite3.go:23 +0x22 fp=0xc000090f78 sp=0xc000090f48 pc=0x4ad9e2
github.com/iamacarpet/go-sqlite3-dynamic.init()
        <autogenerated>:1 +0x93 fp=0xc000090f88 sp=0xc000090f78 pc=0x4b3a33
main.init()
        <autogenerated>:1 +0x54 fp=0xc000090f98 sp=0xc000090f88 pc=0x4b4654
runtime.main()
        /usr/local/go/src/runtime/proc.go:188 +0x1c8 fp=0xc000090fe0 sp=0xc000090f98 pc=0x42b628
runtime.goexit()
        /usr/local/go/src/runtime/asm_amd64.s:1337 +0x1 fp=0xc000090fe8 sp=0xc000090fe0 pc=0x454c71

Is this expected behavior?

Regards,
iamacarpet

Go byte should be C unsigned char

I see that in the README it lists Go's byte and int8 as a C char. However, the Go spec says,

byte alias for uint8

Since uint8 is from [0,255] as is an unsigned char in C I believe that Go's byte should be a C unsigned char.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.