Работа с Go из Python

Вадим Марковцев, source{d}

Работа с Go из Python

goo.gl/SYIzVP

Обо мне

Что мы делаем

Проблема

Go-git

go get -u gopkg.in/src-d/go-git.v3
r, _ := git.NewRepository("https://github.com/...", nil)
_ = r.PullDefault()
iter, _ := r.Commits()
for {
    commit, err := iter.Next()
    if err == io.EOF break
    fmt.Println(commit)
}
		

Внутри...

Потому что

Зависимости

go list -f '{{ .Deps }}' github.com/src-d/go-git

cgo

cgo: вызов наружу

package main
/* #include <stdio.h>
void cprint(const char *what) {
  printf("%s", what);
} */
import "C"
func main() {
  C.cprint(C.CString("Hello, world!\n"))
}

@ Linux

$ CC=wat go build test.go
# command-line-arguments
exec: "wat": executable file not found in $PATH
$ nm test|grep cprint
0000000000454120 T _cgo_efd028656a81_Cfunc_cprint
0000000000401510 t main._Cfunc_cprint
00000000006c2130 d main._cgo_efd028656a81_Cfunc_cprint
0000000000454110 T cprint

Компиляция выполняется хостовым CC и затем объектник линкуется

Что происходит?

StackOverflow
  1. main._Cfunc_cprint
    == вызов
    C.print
    в коде main
  2. runtime.cgocall(_cgo_efd028656a81_Cfunc_cprint, frame)
  3. Далее происходит как-бы syscall; горутина останавливается, вызывается C-заглушка на новом стеке
  4. Заглушка вызывает
    cprint
    с переданными параметрами
  5. Работает
    cprint
  6. Заглушка записывает результат и покидает мир C
  7. На стороне Go происходит возврат из как-бы syscall-а
  8. Горутина продолжает выполняться с результатом от
    cprint

Проблемы

void ...(const char *what) {
  printf("%s", what);
  free(what);
}
 
/* #include <stdlib.h> */
import "C"
func main() {
  C.free(C.CString("..."))
}

cgo: вызов внутрь

go build -o libtest.so -buildmode=c-shared test.go
package main
import "C"
//export goprint
func goprint(what string) { print(what) }
func main() { print("main\n") }
libtest.h  libtest.so

@ Linux

$ nm libtest.so|grep goprint
0000000000061270 T _cgoexp_25e34a201b27_goprint
00000000000b4610 T goprint
00000000000612b0 t main._cgoexpwrap_25e34a201b27_goprint
00000000002fa428 d main._cgoexpwrap_25e34a201b27_goprint.f
 
$ gdb ./test
b goprint

C: вызов наружу

gcc test.c -Wl,-rpath,. -L. -ltest -o test
#include "libtest.h"
int main() {
  GoString str = {"Hello, world!\n", 14};
  goprint(str);
  return 0;
}

Что происходит?

  1. Мы конструируем
    GoString
    (см. первый доклад)
  2. Вызываем
    goprint
    , которая находится в libtest.so
  3. Инициализируется Go рантайм, вызывается
    _cgoexp_25e34a201b27_goprint
    через как-бы syscall
  4. Вызывается
    runtime.cgocallback
    с параметром
    main._cgoexpwrap_25e34a201b27_goprint.f
  5. Попадаем в мир Go, отрабатывает
    main._cgoexpwrap_25e34a201b27_goprint
  6. Выходим из как-бы syscall-а, Go рантайм спит
  7. Продолжает работать
    int main

Проблемы

Go type not supported in export: struct

Проверка указателей в рантайме

var hello string = "hello"
 
//export giveme
func giveme() *string {
  return &hello
}

...

#include <stdio.h>
#include "libtest.h"
int main() {
  GoString *str = giveme();
  printf("%s\n", str->p);
  return 0;
}

@ Linux

$ ./test
panic: runtime error: cgo result has Go pointer
goroutine 17 [running, locked to thread]:
panic(0x7fd4d5a808a0, 0xc82007c020)
	/usr/lib/go-1.6/src/runtime/panic.go:481 +0x3ea
main._cgoexpwrap_b3226aa49989_giveme.func1(0xc82003cee0)
	command-line-arguments/_obj/_cgo_gotypes.go:49 +0x3c
main._cgoexpwrap_b3226aa49989_giveme(0x7fd4d5ad6160)
	command-line-arguments/_obj/_cgo_gotypes.go:51 +0x5b
Aborted (core dumped)

cffi

Оборачиваем пример с goprint

import cffi, re
 
ffi = cffi.FFI()
with open("libtest.h") as fin:
    src = fin.read()

Немного уличной магии

src = re.sub("#ifdef.*\n.*\n#endif|#.*|.*_Complex.*|"
             ".*_check_for_64_bit_pointer_matching_GoInt.*",
             "", src)
src = "extern free(void *ptr);\n" + \
    src.replace("__SIZE_TYPE__", "uintptr_t")

Загружаемся

ffi.cdef(src)
lib = ffi.dlopen("./libtest.so")

Готовим строку

python_str = "Hello!\n".encode("utf-8")
char_ptr = ffi.new("char[]", python_str)
go_str = ffi.new("GoString*", {
    "p": char_ptr,
    "n": len(python_str)
})[0]

Самое главное

lib.goprint(go_str)

Видите как все просто!

 
 
del char_ptr
- заморочки с местным GC

Но ведь в реальной жизни будут объекты?

О да!

Что же делать, ведь указатели запрещены?

Хэндлы

Идея: поддерживать двусторонний мэппинг объектов на 64-битные дескрипторы

func RegisterObject(obj interface{}) Handle
func UnregisterObject(handle Handle) int
func GetObject(handle Handle) (interface{}, bool)
func GetHandle(obj interface{}) (Handle, bool)

Реализация

type Handle uint64
 
var registryHandle2Obj map[Handle]interface{}
var registryObj2Handle ???
Важно: за исключением редких случаев, всегда используем указатель на структуру в качестве
interface{}
, т.к. иначе будет копия.
Проблема: интерфейс не может быть ключом в
map
.

Решение проблемы

var registryObj2Handle map[uintptr][]Handle
Исходник.

Проблема со строками


Лечение состоит в принудительном копировании.

Прочие проблемы

На стороне Python

class GoObject(object):
    ffi = FFI()
    lib = None
    registry = weakref.WeakValueDictionary()
    def __new__(cls, handle, *args, **kwargs):
        assert cls.lib is not None
        instance = GoObject.registry.get(handle)
        if instance is not None:
            return instance
        return object.__new__(cls)
    def __init__(self, handle):
        assert isinstance(handle, int)
        if handle <= 0:
            raise ValueError("Invalid handle")
        self._handle = handle
        GoObject.registry[handle] = self
 
    def __del__(self):
        handle = getattr(self, "_handle", None)
        if handle is not None:
            self.lib.UnregisterObject(handle)
        self._handle = 0

Результат

src-d/gypogit
from gypogit import Repository
r = Repository.New("https://github.com/...")
r.PullDefault()
for c in r.Commits():
    print(c)

Альтернативы

Выводы?

Fin

We are hiring