Введение
В последнее время я увлёкся изучение Go - современного языка програмирования от Google. Первые впечатления от его использования очень приятные и постепенно некоторые свои сервисы я начал переводить с Python на него.
Язык довольно простой и в плане перехода с Python особого дискомфорта не возникает. Cпособ обработок ошибок в Go мне понравился даже больше, чем исключения, но это вкусовщина. Кроме того Go является компилируемым и строго типизированным, что позволяет отловить многие ошибки на этапе компиляции.
В последнее время я работал над продуктом TachoBI для анализа карт тахографа и котнроля режима труда и отдыха водителей, который базируется на ERP системе с открытым исходным кодом Odoo. Так как этот продукт ориентирован на конечных пользователей, то полностью распространять всю систему в исходниках идея не очень и я начал думать, как это можно исправить. Первая идея была написать модули на С и распространять в виде библиотеки к Python приложению, но так как сроки поджимали, а функциональноть была объемная, то эту затею я оставил и стал искать другое решение. Как-то мне пришла идея, а нельзя ли скрестить Python и Go, так как скорость разработки и подерживаемость кода находятся приблизительно на одном уровне. Такое решение было найдено, но без С все-таки не обошлось.
Задача
В качестве примера для иллюстрации я выбрал простую задачку. Пусть на вход в функцию подается строка и ненулевое число, а на выходе получается их конкатенация. Например: “строка” + 1 = “строка1”.
Так как оба языка работают с С, то их взаимодействие мы построим через питоновкую С-шную библиотеку, а к ней с помощью CGO присоединим функцию на Go. Для работы используются Go 1.8 и Python 2.7.
Подготовка функции на Go
Итак файл lib.go, в котором будет функция, выполняющая нашу задачу будет выглядеть вот так:
package main
import (
"errors"
"strconv"
)
func sum(s string, i int) (string, error) {
result := ""
var err error = nil
if i != 0 {
result = s + strconv.Itoa(i)
} else {
err = errors.New("Индекс не может быть 0")
}
return result, err
}
Подготовка C библиотеки для Python
В стандартной документации по Python описано как можно создавать для него C-ные модули. Наша библиотека будет называться test_lib, поэтому скелет нашего модуля будет храниться в файле test_lib.go со следующим содержимым:
package main
/*
#ifdef __linux__
#include <Python.h>
#elif __APPLE__
#include <Python/Python.h>
#endif
PyObject *test_lib_error;
PyObject * Concat(PyObject *, PyObject *);
int Parse_Args(PyObject * args, char * str, int * index) {
return PyArg_ParseTuple(args, "ls", index, str);
}
static PyMethodDef test_lib_methods[] = {
{"concat", Concat, METH_VARARGS, "Test string concat"},
{NULL, NULL, 0, NULL}
};
PyMODINIT_FUNC
inittest_lib(void) {
PyObject *m;
m = Py_InitModule("test_lib", test_lib_methods);
if (m == NULL)
return;
test_lib_error = PyErr_NewException("test_lib.error", NULL, NULL);
Py_INCREF(test_lib_error);
PyModule_AddObject(m, "error", test_lib_error);
}
*/
import "C"
func setException(text string) {
C.PyErr_SetString(C.test_lib_error, C.CString(text))
return
}
func main() {}
Здесь весь С-ный код находится в блоке комментария /**/
, перед импортом “C”. Такой блок называется преамбулой и используется при компиляции С частей пакета. Переменные и функции из преаумбулы можно вызывать в gо коде. Между импортом и комментарием не должно быть пробелов. Подробнее можно посмотреть в документации CGO.
Давайте разберемся что делает преамбула.
Сначала мы присоединяем заголовочный файл Python.h, чтобы получить возможность работать с питоновскими объектами.
Затем мы создаем объекты для обработки ошибок test_lib_error
и нашей будующей функции Concat
, которая и будет реализовывать нашу задачу. Через них будет происходить общение с go-ным кодом.
Функция Parse_Args
будет описана ниже.
Далее объявляем массив test_lib_methods
, который содержит таблицу методов реализуемых расширением, в формате {название, функция, параметры, описание}.
Затем инициализируем модуль функцией inittest_lib
.
Для того чтобы пробросить текс ошибки из Go в питоновское исключение, создается функция setException
. Которая в C-ную переменную test_lib_error
передаст текст, который получит на входе.
Объедиение созданной библиотеки с Go функцией
Мы выполнили все необходимые приготовления, и теперь нам осталось связать нашу функцию sum
, с функцией Concat
в нашей библиотеке.
Для этого в файл lib.go необходимо добавить следующий код:
/*
#cgo pkg-config: python-2.7 --cflags --libs
#ifdef __linux__
#include <Python.h>
#elif __APPLE__
#include <Python/Python.h>
#endif
int Parse_Args(PyObject * args, char * str, int * index);
*/
import "C"
//export Concat
func Concat(self *C.PyObject, args *C.PyObject) *C.PyObject {
var string = new(C.char)
var c_idx C.int
if C.Parse_Args(args, string, &c_idx) == 0 {
setException("Ошибка парсинга аргументов")
return nil
}
result := ""
var err error = nil
if result, err = sum(C.GoString(string), int(c_idx)); err != nil {
setException(err.Error())
}
return C.PyString_FromString(C.CString(result))
}
Как видно тут в преамбуле мы указываем функцию Parse_Args
, которая была объявлена в файле test_lib.go, с ее помощью мы поместим входные параметры в соответствующие переменные для дальнешей работы.
Преамбуда //export
указывает на то, что мы можем использовать эту функцию в коде на С (в файле test_lib.go), в котором она инициализируется добавляется в таблицу функций.
Посмторим что происходит в самой функции. Сначала инициализируются переменные с С-ными типами (различны с типами в Go), затем эти переменные инициализируются входными параметрами, и если опрерация не удалось будет выбрасываться питоновский Exception. Если все прошло нормально и входные параметры мы получили, вызываем нашу функцию sum
для проведения операции. Надо обратить внимание, что перед тем как передать входные параметры в функцию они из С-ных типов приводятся к типам в Go.
Если операция выполнена удачно, то для возврата результата, мы преобразовываем строку Go в сишную строку, а ее в строку питона и уже этот результат вернется нам при вызове эой библиотеки.
Сборка и проверка
Теперь нам осталось собрать библиотеку и проверить ее работу. Для сборки надо выполнить
go build -buildmode=c-shared -o test_lib.so
А проверить можно выполнив команду
python -c 'import test_lib; print(test_lib.Concat('abc', 1))'
P.S. на последней версии MacOS Siera данная операция не выполняется, и пока мне не удалось разобраться почему.
Заключение
Касательно моей основной задачи перевести весь код с Python на Go не заняло у меня много времени. А получившее решение было более чем работоспособным. Кроме того необходимо отметить что такми образом можно использовать функциональность горутин в Python об этом указано в статье[1], но сам я данную штутку не пробывал. Весь исходный код примера лежит можно увидеть на github.
Дополнительные материалы
- BUILDING PYTHON MODULES WITH GO 1.5
- Extending Python with C or C++
- Command cgo
- Пишем модуль расширения для Питона на C
- go-python-module-example