choosing the best adhesive for Go code #1
Одна из сторон языка Go, за которую его многие любят, это простота. Она же является и его слабостью, выражающейся в увеличении количества кода вместе с повышением уровня абстракции.
Я хочу писать более абстрактный код там где это требуется, снимая ограничения системы типов если это необходимо. Для этого в серии статей буду искать такую реализацию скриптового языка, которую можно подружить с Go рантаймом и я точно знаю что хочу нечто lisp-подобное.
Зачем такой язык нужен:
- конфигурация: на скриптовом языке описывается конфигурация приложения
- инициализация: из кода на интерпретируемом языке происходит инициализация компонентов, написанных на языке компилируемом
- расширение: виртуальной машине интерпретируемого языка в некоторые моменты передаётся управление с целью увеличения гибкости
Конфигурация. Такие языки как JSON, YAML, TOML весьма популярны для описания конфигураций программ, но это не всегда удобно и у каждого из них есть свои минусы. В то же время использование языка общего назначения для описания конфигурации может привести к тому что обычно называют “программирование в конфигах”, когда в конфигурационном файле описывается целая программа, сложная для восприятия и появляется необходимость в методах ограничения “мощи” языка. В scheme есть возможность ограничить доступные возможности, что серьёзно выделяет его на фоне других ЯП общего назначения.
Инициализация. При разработке на Go часто получается так что высокая производительность нужна внутри компонентов программ, которые инициализируются при запуске, а дальше работают самостоятельно. В этом месте можно использовать интерпретируемый язык, проводя инициализацию компонентов с его помощью.
Расширение. В тот момент когда нужно предоставить больше гибкости, например реализовав внутри HTTP API ограниченный язык выборки над данными, часто нет возможности использовать язык общего назначения потому что это 1) не безопасно и 2) не удобно/не выразительно для конечного пользователя. Как и в случае с описанием конфигурации, scheme позволяет сильно ограничивать доступное подмножество языка и описывать очень гибкие и простые DSL с помощью макросов.
В этой статье будем пробовать дружить GNU Guile с Go, а в следующих статьях рассмотрим следующие темы(список может меняться по мере продвижения вперёд):
- lua + fennel в Go
- встраиваемые scheme/lisp VM для Go
- …?
Guile
Это не самый очевидный выбор, но он недавно привлёк моё внимание тем что используется внутри guix и раз уж его появление было вызвано решением схожих задач, а я так и не распробовал cgo… все признаки указывают на то что будет интересно, не так ли?
Конечно интероп и скорость исполнения будут не так хороши в виду того что коммуницировать с виртуальной машиной Guile мы будем через cgo.
repl
В официальной документации есть примеры линковки c C кодом и запуска REPL, адаптируем его для Go:
Следующие далее исходники находятся в репозитории. Воспроизвести результат можно выполнив
make run
в директорииrepl
.
package main // #cgo CFLAGS: -g -Wall // #cgo pkg-config: guile-2.2 /* #include <libguile.h> static void ready(void *data, int argc, char **argv) { scm_shell(argc, argv); } void run() { scm_boot_guile(0, NULL, ready, 0); } */ import "C" func main() { C.run() }
Сохраним код в файл main.go
. Чтобы его собрать потребуется libguile
версии 2.2(текущая стабильная на момент написания) и pkg-config
. Где взять всё это добро в вашем дистрибутиве думайте сами, ну а я напишу shell.nix
:
Кстати,
nix
можно использовать внутри docker контейнера:docker run --rm -it nixos/nix /bin/sh
with import <nixpkgs> {}; stdenv.mkDerivation { name = "playground-shell"; buildInputs = [ gnumake guile go pkgconfig clang_7 valgrind ]; shellHook = '' unset GOPATH export CFLAGS="$CFLAGS $(pkg-config --cflags --libs guile-2.2)" ''; }
Собираем:
Можно добавить
-x
флаг чтобы увидеть выполняемые сборщиком команды.
$ go build -o main main.go
REPL запускается:
$ ./main GNU Guile 2.2.3 Copyright (C) 1995-2017 Free Software Foundation, Inc. Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'. This program is free software, and you are welcome to redistribute it under certain conditions; type `,show c' for details. Enter `,help' for help. scheme@(guile-user)> (+ 1 2) $1 = 3 scheme@(guile-user)> (exit)
Посмотрим на размер и линковку:
$ du -hs main 1.1M main $ ldd main linux-vdso.so.1 (0x00007ffe31985000) libguile-2.2.so.1 => /nix/store/...-guile-2.2.3/lib/libguile-2.2.so.1 (0x00007f5ead4d7000) libgc.so.1 => /nix/store/...-boehm-gc-8.0.2/lib/libgc.so.1 (0x00007f5ead467000) libpthread.so.0 => /nix/store/...-glibc-2.27/lib/libpthread.so.0 (0x00007f5ead446000) libc.so.6 => /nix/store/...-glibc-2.27/lib/libc.so.6 (0x00007f5ead290000) libffi.so.6 => /nix/store/...-libffi-3.2.1/lib/../lib64/libffi.so.6 (0x00007f5ead281000) libunistring.so.2 => /nix/store/...-libunistring-0.9.10/lib/libunistring.so.2 (0x00007f5ead0fc000) libgcc_s.so.1 => /nix/store/...-glibc-2.27/lib/libgcc_s.so.1 (0x00007f5eacee6000) libgmp.so.10 => /nix/store/...-gmp-6.1.2/lib/libgmp.so.10 (0x00007f5eace50000) libltdl.so.7 => /nix/store/...-libtool-2.4.6-lib/lib/libltdl.so.7 (0x00007f5eace43000) libcrypt.so.1 => /nix/store/...-glibc-2.27/lib/libcrypt.so.1 (0x00007f5eace09000) libm.so.6 => /nix/store/...-glibc-2.27/lib/libm.so.6 (0x00007f5eacc71000) /nix/store/...-glibc-2.27/lib/ld-linux-x86-64.so.2 => /nix/store/...-glibc-2.27/lib64/ld-linux-x86-64.so.2 (0x00007f5ead60e000) libdl.so.2 => /nix/store/...-glibc-2.27/lib/libdl.so.2 (0x00007f5eacc6c000)
Бинарь не слижком большой, линковка зависимостей libguile
динамическая.
В этом примере “точка входа” в программу была через Go рантайм, после чего управление предавалось в C код, в следующем примере будет наоборот.
shared
Попробуем сделать возможным вызов Go функций из REPL. Для этого будем собирать код с флагом -buildmode=c-shared
, значит функции, которые будут использоваться в REPL, нужно экспортировать:
Следующие далее исходники находятся в репозитории. Воспроизвести результат можно выполнив
make run
в директорииshared
.
package main import "C" //export Plus func Plus(a, b int) int { return a + b } func main() {}
Пробела между
//
иexport
быть не должно. Если поставить пробел то функция не будет экспортирована. Препроцессор C передаёт “привет”.
Теперь создадим из этого кода shared object, который далее будет загружен внутрь Guile:
$ go build -o main.so -buildmode=c-shared main.go
На этом этапе будет создано 2 файла:
main.so
, содержащий Go рантайм и наш кодmain.h
, содержащий декларации типов Go для C
Последний я здесь даже приведу, он есть в индексе моего репозитория:
/* Code generated by cmd/cgo; DO NOT EDIT. */ /* package command-line-arguments */ #line 1 "cgo-builtin-prolog" #include <stddef.h> /* for ptrdiff_t below */ #ifndef GO_CGO_EXPORT_PROLOGUE_H #define GO_CGO_EXPORT_PROLOGUE_H typedef struct { const char *p; ptrdiff_t n; } _GoString_; #endif /* Start of preamble from import "C" comments. */ /* End of preamble from import "C" comments. */ /* Start of boilerplate cgo prologue. */ #line 1 "cgo-gcc-export-header-prolog" #ifndef GO_CGO_PROLOGUE_H #define GO_CGO_PROLOGUE_H typedef signed char GoInt8; typedef unsigned char GoUint8; typedef short GoInt16; typedef unsigned short GoUint16; typedef int GoInt32; typedef unsigned int GoUint32; typedef long long GoInt64; typedef unsigned long long GoUint64; typedef GoInt64 GoInt; typedef GoUint64 GoUint; typedef __SIZE_TYPE__ GoUintptr; typedef float GoFloat32; typedef double GoFloat64; typedef float _Complex GoComplex64; typedef double _Complex GoComplex128; /* static assertion to make sure the file is being used on architecture at least with matching size of GoInt. */ typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1]; typedef _GoString_ GoString; typedef void *GoMap; typedef void *GoChan; typedef struct { void *t; void *v; } GoInterface; typedef struct { void *data; GoInt len; GoInt cap; } GoSlice; #endif /* End of boilerplate cgo prologue. */ #ifdef __cplusplus extern "C" { #endif extern GoInt Plus(GoInt p0, GoInt p1); #ifdef __cplusplus } #endif
Теперь посмотрим на символы из main.so
с помощью nm:
$ go tool nm main.so | grep -F ' T ' 92130 T Plus 923f0 T _cgo_get_context_function 91400 T _cgo_panic ...
“T” - The symbol is in an uninitialized data section for small objects.
Хорошо видно что там присутствует функция Plus
и я не накосячил с //export
в Go коде. Теперь нужно создать main.c
и написать там:
#include <libguile.h> #include "main.h" static SCM plus (SCM a, SCM b) { return scm_from_int((int) Plus(scm_to_int(a), scm_to_int(b))); } static void ready(void *data, int argc, char **argv) { scm_c_define_gsubr("plus", 2, 0, 0, plus); scm_shell(argc, argv); } int main(int argc, char **argv) { scm_boot_guile(argc, argv, ready, 0); return 0; }
Если сделать diff
с предыдущим примером "repl", где похожий C код был внутри main.go
, то:
#include <libguile.h> +#include "main.h" + +static SCM plus (SCM a, SCM b) +{ + scm_from_int((int) Plus(scm_to_int(a), + scm_to_int(b))); +} static void ready(void *data, int argc, char **argv) { + scm_c_define_gsubr("plus", 2, 0, 0, plus); scm_shell(argc, argv); } -void run() { - scm_boot_guile(0, NULL, ready, 0); +int main(int argc, char **argv) +{ + scm_boot_guile(argc, argv, ready, 0); + return 0; }
Объясню что поменялось:
run
заменилmain
, т.е. теперь точка входа не в Go, а в C- начали принимать command-line аргументы и пробрасывать их
scm_boot_guile
, инициализирующей Guile - добавили сишную обертку
plus
для гошной функцииPlus
, которая конвертирует типы - в
ready
добавили вызовscm_c_define_gsubr
чтобы функция-оберткаplus
стала доступна внутри REPL - ну и конечно же загрузили сгенерированный cgo
main.h
, без которого ничего не будет работать
Функции
scm_*
неплохо описаны в guile reference.
С main.so
уже можно собрать бинарь, который запустит REPL, и проверить что функция plus
доступна:
$ clang -o main main.c ./main.so `pkg-config --cflags --libs guile-2.2` $ ./main GNU Guile 2.2.3 Copyright (C) 1995-2017 Free Software Foundation, Inc. Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'. This program is free software, and you are welcome to redistribute it under certain conditions; type `,show c' for details. Enter `,help' for help. scheme@(guile-user)> (plus 1 2) $1 = 3 scheme@(guile-user)> (exit)
Взглянем на размер бинаря и линковку:
$ du -hs main.so 1.4M main.so $ du -hs main 24K main $ ldd main linux-vdso.so.1 (0x00007ffe31985000) ./main.so (0x00007fab3ff52000) libguile-2.2.so.1 => /nix/store/...-guile-2.2.3/lib/libguile-2.2.so.1 (0x00007f5ead4d7000) libgc.so.1 => /nix/store/...-boehm-gc-8.0.2/lib/libgc.so.1 (0x00007f5ead467000) libpthread.so.0 => /nix/store/...-glibc-2.27/lib/libpthread.so.0 (0x00007f5ead446000) libc.so.6 => /nix/store/...-glibc-2.27/lib/libc.so.6 (0x00007f5ead290000) libffi.so.6 => /nix/store/...-libffi-3.2.1/lib/../lib64/libffi.so.6 (0x00007f5ead281000) libunistring.so.2 => /nix/store/...-libunistring-0.9.10/lib/libunistring.so.2 (0x00007f5ead0fc000) libgcc_s.so.1 => /nix/store/...-glibc-2.27/lib/libgcc_s.so.1 (0x00007f5eacee6000) libgmp.so.10 => /nix/store/...-gmp-6.1.2/lib/libgmp.so.10 (0x00007f5eace50000) libltdl.so.7 => /nix/store/...-libtool-2.4.6-lib/lib/libltdl.so.7 (0x00007f5eace43000) libcrypt.so.1 => /nix/store/...-glibc-2.27/lib/libcrypt.so.1 (0x00007f5eace09000) libm.so.6 => /nix/store/...-glibc-2.27/lib/libm.so.6 (0x00007f5eacc71000) /nix/store/...-glibc-2.27/lib/ld-linux-x86-64.so.2 => /nix/store/...-glibc-2.27/lib64/ld-linux-x86-64.so.2 (0x00007f5ead60e000) libdl.so.2 => /nix/store/...-glibc-2.27/lib/libdl.so.2 (0x00007f5eacc6c000)
Видно что сишный бинарь с динамической линковкой занимает не много места, в то же время собранный Go main.so
занимает больше места чем бинарь до этого(кстати я пока точно не знаю почему).
В этом примере складываются 2 числа, что может быть полезно для демонстрации концепций, но очень далеко от реальных задач. Как насчёт взаимодействия с user-defined структурами данных?
structs-simple
Например если в Go коде будет такой тип:
//export Post type Post struct { Title string Body Body } type Body struct { Type string Text string }
То ничего работать не будет:
./main.go:5:11: Go type not supported in export: struct { Title string Body Body }
Потому что cgo умеет работать только с “простыми” типами и не похоже что это изменится в ближайшее время.
Раз Go не может сделать это за человека, придётся написать такую же структуру самостоятельно, начнём с чего-нибудь попроще, например структуры для двух операндов в main.go
:
Следующие далее исходники находятся в репозитории. Воспроизвести результат можно выполнив
make run
в директорииstructs-simple
.
C.struct_operands
представляет структуруoperands
, которая определена в C.
package main // #cgo CFLAGS: -g -Wall /* struct operands { int a; int b; }; */ import "C" import ( "fmt" ) type operands struct { a int32 b int32 } func CPlus(ops C.struct_operands) C.int { return C.int(Plus( operands{ a: int32(ops.a), b: int32(ops.b), }, )) } func Plus(ops operands) int32 { return ops.a + ops.b; } func main() { cops := C.struct_operands{a: 1, b: 2} fmt.Printf("%+v\n", CPlus(cops)) }
Функция Plus
выполняет сложение чисел. Её входные данные и результат обёрнуты CPlus
, которая приводит типы из C-совместимых в Go-совместимые.
Для простоты считаем что
int
всегда 32-bit, но лучше так не писать, ведь это не всегда правда).
Соберём и запустим пример:
$ go build -o main main.go $ ./main 3
structs-complex
Настало время усложнить структуры данных, введя строки. Посмотрим на изначальный пример “сложных” структур данных body
& page
в main.go
:
Следующие далее исходники находятся в репозитории. Воспроизвести результат можно выполнив
make run
в директорииstructs-complex
.
package main // #cgo CFLAGS: -g -Wall /* struct body { char *format; char *text; }; struct page { char *title; struct body body; }; */ import "C" import ( "fmt" "github.com/davecgh/go-spew/spew" ) type body struct { format string text string } type page struct { title string body body } func CCreate(p C.struct_page) { Create(page{ title: C.GoString(p.title), body: body{ format: C.GoString(p.body.format), text: C.GoString(p.body.text), }, }) } func Create(p page) { fmt.Println("requested to create a page:") spew.Dump(p) } func main() { p := C.struct_page{ title: C.CString("hello"), body: C.struct_body{ format: C.CString("markdown"), text: C.CString("hello world"), }, } CCreate(p) }
Принцип остался темже, но хочется обратить внимание на то что cgo нагенерил специальный тип для своих строк, так что достаточно удобно конвертировать сишный char*
в гошный string
.
Всё управление памятью в примерах structs-simple и structs-complex автоматическое, в “сишную часть программы” лишь передаются ссылки на память, но не наоборот. И сишному коду не нужно держать ссылки на эту память, в противном случае код был бы сложнее(привет malloc, как минимум).
После запуска можем наблюдать следующее:
$ go build -o main main.go $ ./main requested to create a page: (main.page) { title: (string) (len=5) "hello", body: (main.body) { format: (string) (len=8) "markdown", text: (string) (len=11) "hello world" } }
structs-guile
Теперь “прикрутим” Guile к этому коду и будем создавать структуры page
& body
из интерпретируемого языка, экспортируя нужные функции из main.go
:
Следующие далее исходники находятся в репозитории. Воспроизвести результат можно выполнив
make run
в директорииstructs-guile
.
package main // #cgo CFLAGS: -g -Wall // #include "structs.h" import "C" import ( "fmt" "time" "github.com/davecgh/go-spew/spew" ) type body struct { format string text string } type page struct { title string body body } func printer(p *C.struct_page) { for { time.Sleep(time.Second * 5) spew.Dump(p) } } //export CCreate func CCreate(p *C.struct_page) { Create(page{ title: C.GoString(p.title), body: body{ format: C.GoString(p.body.format), text: C.GoString(p.body.text), }, }) go printer(p) } func Create(p page) { fmt.Println("requested to create a page:") spew.Dump(p) } func main() {}
Почти тоже самое что и в structs-complex, за исключением двух вещей:
- структуры переехали в
structs.h
- наружу экспортируется
CCreate
Вот содержимое structs.h
#ifndef STRUCTS_H
и другие директивы препроцессора тут это т.н. include guards.
#ifndef STRUCTS_H #define STRUCTS_H struct body { char *format; char *text; }; struct page { char *title; struct body *body; }; #endif
На данном этапе уже можно собрать main.so
и проверить что CCreate
действительно экспортируется:
$ go build -o main.so -buildmode=c-shared main.go $ go tool nm main.so | grep -F ' T ' 130f60 T CCreate 131200 T _cgo_get_context_function d44f0 T _cgo_panic ...
А теперь будем программировать ни C, “вклеивая” нужные структуры данных в Guile, этот код будет расположен в main.c
:
#include <libguile.h> #include "structs.h" #include "main.h" static SCM body_type; static SCM page_type; void init_body_type(void) { body_type = scm_make_foreign_object_type(scm_from_utf8_symbol("body"), scm_list_1(scm_from_utf8_symbol("data")), NULL); } void init_page_type(void) { page_type = scm_make_foreign_object_type(scm_from_utf8_symbol("page"), scm_list_1(scm_from_utf8_symbol("data")), NULL); } void init_foreign_types(void) { init_body_type(); init_page_type(); } SCM make_body(SCM format, SCM text) { struct body *body; body = (struct body *) scm_gc_malloc(sizeof(struct body), "body"); body->format = scm_to_stringn(format, NULL, "UTF-8", SCM_FAILED_CONVERSION_ERROR); body->text = scm_to_stringn(text, NULL, "UTF-8", SCM_FAILED_CONVERSION_ERROR); return scm_make_foreign_object_1(body_type, body); } SCM make_page(SCM title, SCM body) { struct page *page; page = (struct page *) scm_gc_malloc(sizeof(struct page), "page"); page->title = scm_to_stringn(title, NULL, "UTF-8", SCM_FAILED_CONVERSION_ERROR); page->body = (struct body *) scm_foreign_object_ref(body, 0); return scm_make_foreign_object_1(page_type, page); } SCM create_page(SCM page) { CCreate((struct page *) scm_foreign_object_ref(page, 0)); return SCM_UNSPECIFIED; } static void ready(void *data, int argc, char **argv) { init_foreign_types(); scm_c_define_gsubr("make-body", 2, 0, 0, make_body); scm_c_define_gsubr("make-page", 2, 0, 0, make_page); scm_c_define_gsubr("create-page", 1, 0, 0, create_page); scm_shell(argc, argv); } int main(int argc, char **argv) { scm_boot_guile(argc, argv, ready, 0); return 0; }
Разберём код:
Как и в предыдущем примере под названием shared, в scm_boot_guile
передаётся ready
, где в Guile экспортируются конструкторы(make_*
) структур данных, написанные на C. Эти конструкторы будут доступны в REPL как функции.
Также помимо конструкторов можно заметить что в REPL экспортируется функция create_page
, которая оборачивает CCreate
из Go, передавая ей сишную структуру page
, таким образом вызов Go из Guile ничем не отличает от вызова Go и C.
Каждый из конструкторов соответствующего типа(make_*
) аллоцирует память внутри Guile под сишную структуру page
или body
, полученная память передаётся в управление Guile и управляется его сборщиком мусора.
Здесь есть проблема, которая себя обязательно проявит если Go код будет использовать переданный ему указатель уже после вызова
CCreate
. Указатель наstruct page
, отдаваемый вCCreate
, может быть собран на одном из проходов Guile GC(скорее всего на втором, потому что mark-sweep). Позже я покажу как можно сломать этот код :)
Теперь можно собрать программу, подключив к ней main.so
, и запустить:
$ clang -Wall -o main main.c ./main.so `pkg-config --cflags --libs guile-2.2` $ ./main GNU Guile 2.2.3 Copyright (C) 1995-2017 Free Software Foundation, Inc. Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'. This program is free software, and you are welcome to redistribute it under certain conditions; type `,show c' for details. Enter `,help' for help. scheme@(guile-user)>
Напишем в REPL тестовый код, который напечатает структуру данных, которая была получена на стороне Go:
scheme@(guile-user)> (create-page (make-page "title" (make-body "markdown" "hello world!"))) requested to create a page: (main.page) { title: (string) (len=5) "title", body: (main.body) { format: (string) (len=8) "markdown", text: (string) (len=12) "hello world!" } }
Настало время сломать эту программу, продемонстрировав проблемы в управлении памятью. Вот patch для structs-guile/main.go
:
diff --git a/main.go b/main.go --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import "C" import ( "fmt" + "time" "github.com/davecgh/go-spew/spew" ) @@ -19,6 +20,13 @@ type page struct { body body } +func printer(p *C.struct_page) { + for { + time.Sleep(time.Second * 5) + spew.Dump(p) + } +} + //export CCreate func CCreate(p *C.struct_page) { Create(page{ @@ -28,6 +36,7 @@ func CCreate(p *C.struct_page) { text: C.GoString(p.body.text), }, }) + go printer(p) } func Create(p page) {
Запустив отдельную горутину в конце CCreate
, которая каждые 5 секунд пытается сдампить содержимое переданного в CCreate
указателя, будет легко увидеть как программа упадёт после того как указатель будет собран сборщиком мусора.
Теперь достаточно пересобрать main.so
, вновь запустить REPL и выполнить там следующий код:
Может потребоваться больше чем 2 вызовов
(gc)
, инициирующих сборку мусора.
scheme@(guile-user)> (create-page (make-page "title" (make-body "markdown" "hello world!"))) requested to create a page: (main.page) { title: (string) (len=5) "title", body: (main.body) { format: (string) (len=8) "markdown", text: (string) (len=12) "hello world!" } } scheme@(guile-user)> (gc) scheme@(guile-user)> (gc) scheme@(guile-user)> (*main._Ctype_struct_page)(0x7f274dc5b670)({ title: (*main._Ctype_char)(0x7f276c5999e0)(5), body: (*main._Ctype_struct_body)(0x304)({ format: panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x304 pc=0x7f27774860ab] goroutine 5 [running]: reflect.Value.IsNil(...) /nix/store/...-go-1.12/share/go/src/reflect/value.go:1041 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc0000c3f48, 0x7f27774d5e40, 0x304, 0x1b6) /home/user/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:101 +0x81b ...
Есть два способа это исправить:
- копировать данные, с которыми предполагается взаимодейстовать в фоне
- управлять памятью вручную, т.е. используя системный аллокатор и в Guile, и в Go
Но эти примеры я уже не буду показывать, потому что есть способ лучше.
ffi-simple
В Guile хорошо развит FFI. Его можно использовать для вызова экспортируемых из main.go
функций. Начнём вновь с простых примеров с функцией Plus
:
Следующие далее исходники находятся в репозитории. Воспроизвести результат можно выполнив
make run
в директорииffi-simple
.
package main import "C" //export Plus func Plus(a, b C.int) C.int { return C.int(int32(a)+int32(b)) } func main() {}
Только теперь загружать main.so
будем сразу из Guile, определив модуль в main.scm
:
При запуске примера необходимо добавить директорию
ffi-simple
в переменную окружениеLD_LIBRARY_PATH
. Кстати,make run
делает это автоматически.
(define-module (main) #:use-module (system foreign) #:export (plus)) (define lib (dynamic-link "main")) (define plus (pointer->procedure int (dynamic-func "Plus" lib) (list int int)))
За динамическую загрузку библиотеки отвечает функция dynamic-link
. После её вызова я определяю функцию plus
, которая вызывает Plus
из main.so
.
Ну и раз уж начал использовать систему модулей Guile, удобнее будет и пример кода для использования функции plus
записать в отдельный файл, run.scm
:
(add-to-load-path (getcwd)) (use-modules (main)) (display (plus 1 2)) (newline)
Запускаем:
$ go build -o main.so -buildmode=c-shared main.go $ guile -s ./run.scm 3
ffi-complex
Вернемся к примерам с структурами body
& page
. Начнем с описания main.go
:
Следующие далее исходники находятся в репозитории. Воспроизвести результат можно выполнив
make run
в директорииffi-complex
.
package main // #cgo CFLAGS: -g -Wall /* struct body { char *format; char *text; }; struct page { char *title; struct body *body; }; */ import "C" import ( "fmt" "github.com/davecgh/go-spew/spew" ) type body struct { format string text string } type page struct { title string body body } //export CCreate func CCreate(p *C.struct_page) { Create(page{ title: C.GoString(p.title), body: body{ format: C.GoString(p.body.format), text: C.GoString(p.body.text), }, }) } func Create(p page) { fmt.Println("requested to create a page:") spew.Dump(p) } func main() {}
Этот код такой же как в structs-guile, разница только в том что здесь сишные структуры определены прямо в main.go
.
Далее опишем для CCreate
Guile FFI в main.scm
:
(define-module (main) #:use-module (system foreign) #:export (make-body make-page create-page)) (define lib (dynamic-link "main")) (define (make-body format text) (make-c-struct '(* *) (list (string->pointer format) (string->pointer text)))) (define (make-page title body) (make-c-struct '(* *) (list (string->pointer title) body))) (define create-page (let ((create (pointer->procedure void (dynamic-func "CCreate" lib) '(*)))) (lambda (page) (create page))))
Добавилось две новых функции-конструктора:
make-body
make-page
Каждая из них создаёт сишную структуру, стараясь максимально соответствовать указанным в main.go
сишным типам. Автоматически это не энфорсится, проверок нет, только рантайм :)
По аналогии с предыдущим примером запишем код для запуска примера в run.scm
:
(add-to-load-path (getcwd)) (use-modules (main)) (create-page (make-page "title" (make-body "markdown" "hello world!")))
Произведём сборку и запуск:
$ go build -o main.so -buildmode=c-shared main.go $ guile -s ./run.scm requested to create a page: (main.page) { title: (string) (len=5) "title", body: (main.body) { format: (string) (len=8) "markdown", text: (string) (len=12) "hello world!" } }
summary
Подведём итоги. Запускать Go код из скриптового языка через C конечно можно, но не удобно. Количество C кода для “склеивания” структур данных в structs-guile впечатляет и подтверждает что это совсем не тот инструмент, которым стоит решать поставленную задачу. Также не стоит забывать что FFI в Go это вообще не быстро.