Введение

В последнее время я увлёкся изучение 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.

Дополнительные материалы

  1. BUILDING PYTHON MODULES WITH GO 1.5
  2. Extending Python with C or C++
  3. Command cgo
  4. Пишем модуль расширения для Питона на C
  5. go-python-module-example