Вызов функций Go из других языков
- среда, 22 марта 2017 г. в 03:14:06
С версии 1.5 компилятор Go поддерживает несколько режимов сборки, определяемых флагом buildmode
. Их ещё называют режимами исполнения Go (Go Execution Modes). С их помощью go tool
может компилировать пакеты Go в нескольких форматах, включая архивы и библиотеки общего пользования Go (shared libraries), архивы и библиотеки общего пользования Си, а с версии 1.8 — и динамические плагины Go.
В статье мы рассмотрим компилирование пакетов Go в библиотеки Си. В этом режиме сборки компилятор генерирует стандартный бинарный файл объекта (shared object) (.so), передавая функции Go в качестве API в стиле Си. Мы поговорим о том, как создавать библиотеки Go, которые можно вызывать из C, Python, Ruby, Node и Java.
Весь код доступен на GitHub.
Сначала напишем код на Go. Допустим, у нас есть библиотека awesome, цель — сделать её доступной для других языков. Прежде чем компилировать код в библиотеку, нужно соблюсти четыре условия:
main
. Тогда компилятор соберёт его и все зависимости в один бинарный файл общего объекта."C"
.//export
.main
.Следующий источник Go экспортирует четыре функции: Add
, Cosine
, Sort
и Log
. Нужно признаться, что библиотека awesome не столь впечатляющая. Однако её разнообразные сигнатуры функций (function signatures) помогут нам изучить возможные последствия отображения типов (type mapping).
Файл awesome.go:
package main
import "C"
import (
"fmt"
"math"
"sort"
"sync"
)
var count int
var mtx sync.Mutex
//export Add
func Add(a, b int) int {
return a + b
}
//export Cosine
func Cosine(x float64) float64 {
return math.Cos(x)
}
//export Sort
func Sort(vals []int) {
sort.Ints(vals)
}
//export Log
func Log(msg string) int {
mtx.Lock()
defer mtx.Unlock()
fmt.Println(msg)
count++
return count
}
func main() {}
Пакет скомпилирован с флагом -buildmode=c-shared
, чтобы создать бинарный файл объекта:
go build -o awesome.so -buildmode=c-shared awesome.go
Компилятор создаёт заголовочный С-файл awesome.h
и файл объекта awesome.so
:
-rw-rw-r — 1362 Feb 11 07:59 awesome.h
-rw-rw-r — 1997880 Feb 11 07:59 awesome.so
Обратите внимание, что размер файла .so
около 2 Мб. Довольно много для такой маленькой библиотеки. Дело в том, что в этот файл запихивается вся runtime-механика Go и зависимые пакеты.
Он определяет С-типы, которые с помощью семантики cgo мапятся в совместимые Go-типы.
/* Created by “go tool cgo” — DO NOT EDIT. */
...
typedef long long GoInt64;
typedef unsigned long long GoUint64;
typedef GoInt64 GoInt;
typedef double GoFloat64;
...
typedef struct { const char *p; GoInt n; } GoString;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
...
#endif
...
extern GoInt Add(GoInt p0, GoInt p1);
extern GoFloat64 Cosine(GoFloat64 p0);
extern void Sort(GoSlice p0);
extern GoInt Log(GoString p0);
...
Это 64-битный бинарный ELF-файл общего объекта. Можно верифицировать его содержимое с помощью команды file
.
$> file awesome.so
awesome.so: ELF 64-bit LSB shared object, x86–64, version 1 (SYSV), dynamically linked, BuildID[sha1]=1fcf29a2779a335371f17219fffbdc47b2ed378a, not stripped
С помощью команд nm
и grep
можно проверить, что наши функции Go экспортированы в файл объекта.
$> nm awesome.so | grep -e "T Add" -e "T Cosine" -e "T Sort" -e "T Log"
00000000000d0db0 T Add
00000000000d0e30 T Cosine
00000000000d0f30 T Log
00000000000d0eb0 T Sort
Есть два способа использования библиотеки для вызова функций Go из Си. Сначала привязать (bind) библиотеку: статически — на стадии компилирования, динамически — во время runtime. Либо динамически загружать и связывать (bound) символы функции Go.
В данном случае мы используем заголовочный файл для создания статических связей между типами и функциями, экспортируемыми в файле объекта. Код получается простым и чистым (опущены некоторые выражения print):
Файл client1.c
#include <stdio.h>
#include "awesome.h"
int main() {
printf("Using awesome lib from C:\n");
GoInt a = 12;
GoInt b = 99;
printf("awesome.Add(12,99) = %d\n", Add(a, b));
printf("awesome.Cosine(1) = %f\n", (float)(Cosine(1.0)));
GoInt data[6] = {77, 12, 5, 99, 28, 23};
GoSlice nums = {data, 6, 6};
Sort(nums);
...
GoString msg = {"Hello from C!", 13};
Log(msg);
}
Теперь компилируем С-код с указанием библиотеки объектов:
$> gcc -o client client1.c ./awesome.so
Когда запускается получившийся бинарный файл, он линкуется с библиотекой awesome.so, вызывает экспортированные из Go функции и выдаёт:
$> ./client
awesome.Add(12,99) = 111
awesome.Cosine(1) = 0.540302
awesome.Sort(77,12,5,99,28,23): 5,12,23,28,77,99,
Hello from C!
При таком подходе С-код использует библиотеку загрузчика динамических связей (dynamic link loader library) для динамической загрузки и привязки экспортированных символов. Определённые в dhfcn.h
функции применяются:
Эта версия будет длиннее, поскольку привязка и линкование выполняются в вашем исходном коде. Но она делает всё то же самое, что и предыдущий вариант (опущены некоторые выражения обработки ошибок и print):
Файл client2.c
#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>
// define types needed
typedef long long go_int;
typedef double go_float64;
typedef struct{void *arr; go_int len; go_int cap;} go_slice;
typedef struct{const char *p; go_int len;} go_str;
int main(int argc, char **argv) {
void *handle;
char *error;
handle = dlopen ("./awesome.so", RTLD_LAZY);
if (!handle) {
fputs (dlerror(), stderr);
exit(1);
}
go_int (*add)(go_int, go_int) = dlsym(handle, "Add");
if ((error = dlerror()) != NULL) { ... }
go_int sum = (*add)(12, 99);
printf("awesome.Add(12, 99) = %d\n", sum);
go_float64 (*cosine)(go_float64) = dlsym(handle, "Cosine");
go_float64 cos = (*cosine)(1.0);
printf("awesome.Cosine(1) = %f\n", cos);
void (*sort)(go_slice) = dlsym(handle, "Sort");
go_int data[5] = {44,23,7,66,2};
go_slice nums = {data, 5, 5};
sort(nums);
go_int (*log)(go_str) = dlsym(handle, "Log");
go_str msg = {"Hello from C!", 13};
log(msg);
dlclose(handle);
}
В предыдущем коде мы определили наше собственное подмножество совместимых с Go типов С: go_int
, go_float
, go_slice
и go_str
. Для загрузки символов Add
, Cosine
, Sort
и Log
с последующей привязкой к соответствующим указателям функций мы использовали dlsym
. Затем скомпилировали код, сославшись на библиотеку dl
(не awesome.so):
$> gcc -o client client2.c -ldl
При выполнении кода бинарный файл С загружает и линкует библиотеку awesome.so. В результате получаем:
$> ./client
awesome.Add(12, 99) = 111
awesome.Cosine(1) = 0.540302
awesome.Sort(44,23,7,66,2): 2,7,23,44,66,
Hello from C!
С Python всё несколько проще. Мы используем библиотеку внешних функций (foreign function library) ctypes
, чтобы вызвать функции Go из библиотеки awesome.so, как показано в следующем примере (опущены некоторые выражения print):
Файл client.py
from ctypes import *
lib = cdll.LoadLibrary("./awesome.so")
lib.Add.argtypes = [c_longlong, c_longlong]
print "awesome.Add(12,99) = %d" % lib.Add(12,99)
lib.Cosine.argtypes = [c_double]
lib.Cosine.restype = c_double
cos = lib.Cosine(1)
print "awesome.Cosine(1) = %f" % cos
class GoSlice(Structure):
_fields_ = [("data", POINTER(c_void_p)),
("len", c_longlong), ("cap", c_longlong)]
nums = GoSlice((c_void_p * 5)(74, 4, 122, 9, 12), 5, 5)
lib.Sort.argtypes = [GoSlice]
lib.Sort.restype = None
lib.Sort(nums)
class GoString(Structure):
_fields_ = [("p", c_char_p), ("n", c_longlong)]
lib.Log.argtypes = [GoString]
msg = GoString(b"Hello Python!", 13)
lib.Log(msg)
Обратите внимание, что переменная lib
представляет загруженные символы из файла разделяемого объекта. Объявим классы Python GoString
и GoSlice
, чтобы сопоставить их с соответствующими структурными типами С (struct types). При выполнении кода Python вызываются функции Go из общего объекта:
$> python client.py
awesome.Add(12,99) = 111
awesome.Cosine(1) = 0.540302
awesome.Sort(74,4,122,9,12) = [ 4 9 12 74 122 ]
Hello Python!
Здесь всё делается по тому же принципу, что и выше. Используем FFI gem для динамической загрузки и вызова экспортированных функций из awesome.so.
Файл client.rb
require 'ffi'
module Awesome
extend FFI::Library
ffi_lib './awesome.so'
class GoSlice < FFI::Struct
layout :data, :pointer,
:len, :long_long,
:cap, :long_long
end
class GoString < FFI::Struct
layout :p, :pointer,
:len, :long_long
end
attach_function :Add, [:long_long, :long_long], :long_long
attach_function :Cosine, [:double], :double
attach_function :Sort, [GoSlice.by_value], :void
attach_function :Log, [GoString.by_value], :int
end
print "awesome.Add(12, 99) = ", Awesome.Add(12, 99), "\n"
print "awesome.Cosine(1) = ", Awesome.Cosine(1), "\n"
nums = [92,101,3,44,7]
ptr = FFI::MemoryPointer.new :long_long, nums.size
ptr.write_array_of_long_long nums
slice = Awesome::GoSlice.new
slice[:data] = ptr
slice[:len] = nums.size
slice[:cap] = nums.size
Awesome.Sort(slice)
msg = "Hello Ruby!"
gostr = Awesome::GoString.new
gostr[:p] = FFI::MemoryPointer.from_string(msg)
gostr[:len] = msg.size
Awesome.Log(gostr)
В Ruby нам нужно расширить модуль FFI
, чтобы объявить символы загружаемыми из общей библиотеки. Объявим классы Ruby GoSlice
и GoString
, чтобы сопоставить их с соответствующими С-структурами. При выполнении кода он вызывает экспортированные функции Go:
$> ruby client.rb
awesome.Add(12, 99) = 111
awesome.Cosine(1) = 0.5403023058681398
awesome.Sort([92, 101, 3, 44, 7]) = [3, 7, 44, 92, 101]
Hello Ruby!
Мы воспользуемся библиотекой внешних функций node-ffi (и парочкой зависимых пакетов) для динамической загрузки и вызова экспортированных функций Go из awesome.so:
Файл client.js
var ref = require("ref");
var ffi = require("ffi");
var Struct = require("ref-struct");
var ArrayType = require("ref-array");
var LongArray = ArrayType(ref.types.longlong);
var GoSlice = Struct({
data: LongArray,
len: "longlong",
cap: "longlong"
});
var GoString = Struct({
p: "string",
n: "longlong"
});
var awesome = ffi.Library("./awesome.so", {
Add: ["longlong", ["longlong", "longlong"]],
Cosine: ["double", ["double"]],
Sort: ["void", [GoSlice]],
Log: ["longlong", [GoString]]
});
console.log("awesome.Add(12, 99) = ", awesome.Add(12, 99));
console.log("awesome.Cosine(1) = ", awesome.Cosine(1));
nums = LongArray([12,54,0,423,9]);
var slice = new GoSlice();
slice["data"] = nums;
slice["len"] = 5;
slice["cap"] = 5;
awesome.Sort(slice);
str = new GoString();
str["p"] = "Hello Node!";
str["n"] = 11;
awesome.Log(str);
Для объявления и загрузки символов из разделяемой библиотеки Node использует объект ffi
. Также объявим структурные объекты Node GoSlice
и GoString
, чтобы сопоставить их с соответствующими С-структурами. При выполнении кода вызываются экспортированные функции:
awesome.Add(12, 99) = 111
awesome.Cosine(1) = 0.5403023058681398
awesome.Sort([12,54,9,423,9] = [ 0, 9, 12, 54, 423 ]
Hello Node!
Для вызова экспортированных функций воспользуемся библиотекой Java Native Access (JNA) (некоторые выражения опущены или даны аббревиатурами):
Файл Client.java
import com.sun.jna.*;
public class Client {
public interface Awesome extends Library {
public class GoSlice extends Structure {
...
public Pointer data;
public long len;
public long cap;
}
public class GoString extends Structure {
...
public String p;
public long n;
}
public long Add(long a, long b);
public double Cosine(double val);
public void Sort(GoSlice.ByValue vals);
public long Log(GoString.ByValue str);
}
static public void main(String argv[]) {
Awesome awesome = (Awesome) Native.loadLibrary(
"./awesome.so", Awesome.class);
System.out.printf(... awesome.Add(12, 99));
System.out.printf(... awesome.Cosine(1.0));
long[] nums = new long[]{53,11,5,2,88};
Memory arr = new Memory(... Native.getNativeSize(Long.TYPE));
Awesome.GoSlice.ByValue slice = new Awesome.GoSlice.ByValue();
slice.data = arr;
slice.len = nums.length;
slice.cap = nums.length;
awesome.Sort(slice);
Awesome.GoString.ByValue str = new Awesome.GoString.ByValue();
str.p = "Hello Java!";
str.n = str.p.length();
awesome.Log(str);
}
}
Для использования JNA определим Java-интерфейс Awesome, который будет представлять загруженные из awesome.so символы. Также объявим классы GoSlice
и GoString
, чтобы сопоставить их с соответствующими С-структурами. Компилируем и запускаем код, вызываются экспортированные функции:
$> javac -cp jna.jar Client.java
$> java -cp .:jna.jar Client
awesome.Add(12, 99) = 111
awesome.Cosine(1.0) = 0.5403023058681398
awesome.Sort(53,11,5,2,88) = [2 5 11 53 88 ]
Hello Java!
Мы рассмотрели, как создавать библиотеку Go для её использования другими языками. Компилируя пакеты Go в библиотеки в стиле Си, вы с помощью внутрипроцессной (in-process) интеграции бинарных файлов объектов можете легко обращаться к своим Go-проектам совместно с Cи, Python, Ruby, Node, Java и т. д. Поэтому в следующий раз, когда вы сделаете супер-API на Go, не забудьте поделиться им с разработчиками на других языках.