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 это вообще не быстро.

references