четверг, 13 июля 2017 г.

Использование C-библиотек из Golang 1.8 с callback-функцией и cross-компиляция

Выяснил для себя, что писать системные программы на Golang гораздо приятнее, чем на С. Расплата - минимальный размер программы в 1 Мбайт, в отличие от десятков Кбайт на чистом C. Жалко, конечно, но C требует слишком много лишнего в современном программировании.
Поэтому перевожу сервер поддержки протокола DALI и управления освещением (под Raspberry) с C на Go.
Сложности: как я писал ранее, в сервере используется библиотека pigpio. Поэтому нужен вызов C из Golang. Но мало того, нужен callback на Go-функцию из C. Настоящий хардкор - все это хочется в cross-компиляции.

Пример на Go с вызовом C и Go-callback

В исходнике ниже три примера:
1) вызов функции setCallback C-библиотеки extclib из Go
2) передача в С указателя на Go-функцию goCallback
3) создание C-функции myCFunc прямо в Go-исходном файле с последующим вызовом ее из Go

package main
/*
// (1) подключение внешней C-библиотеки к линковке
// (1) объектный файл extclib, скомпилированный под целевую платформу,
// (1) а также extclib.h, должны быть доступны локально (см. ниже
// (1) "Настройка cross-компиляции")
#cgo LDFLAGS: -lextclib
// (1) подключение заголовочного файла внешней C-библиотеки
// (1) в нем описана функция setCallback (см. ниже)
#include <extclib.h>
#include <stdint.h>
// (2) описание callback-функции на Go в C, чтобы можно было передать в C указатель на нее
extern void goCallback(unsigned data);

#ifndef MYCINCL
#define MYCINCL 1
// (3) static inline - необходимо для предотвращения ошибки компиляции
// (3) о повторном объявлении функции myCFunc
static inline void myCFunc(uint32_t data){
// здесь C-код
}
#endif
*/
import "C"


// (1) вызов C-функции setCallback с передачей указателя на функцию Go
func installCallback(){
C.setCallback((*[0]byte)(C.goCallback))
}

// (2) ниже комментарий "//export goCallback" указывает Go, что функция будет видна из С
// (2) внимание! Пробелов между // и export быть не должно, иначе Go не сделает экспорт
//export goCallback
func goCallback(data C.uint){
}

// (3) вызов C-функции из Go
func callCFunc(data uint32){
C.myCFunc(C.uint32_t(data))
}

Настройка cross-компиляции

Для cross-компиляции необходимо локальное наличие компилятора под целевую платформу, заголовочных C-файлов, которые будут использоваться и объектных файлов для линковки (скомпилированных под целевую платформу).
Например, можно установить GNU Toolchain for Raspberry (http://gnutoolchains.com/raspberry/tutorial), который умеет забирать заголовочные и объектные файлы, установленные на Raspberry, себе для локальной cross-компиляции.

Дальше можно сделать bat-файл для компиляции или выставить переменные окружения.
Выставить путь к cross-компилятору, который находится внутри toolchain:
@set CC=arm-linux-gnueabihf-gcc

Установить архитектуру. Если этого не сделать, то получим ошибку вроде "unrecognized command line option '-m64'":
@set GOARCH=arm
@set GOARM=6
и операционную систему, иначе ошибка "unsupported GOOS/GOARCH pair windows/arm":
@set GOOS=linux

Выставить CGO_ENABLED, потому что по-умолчанию она выключена для cross-компиляции (и go build в этом случае молча игнорирует файлы, где есть import "C" - даже ошибки в них не проверяет):
@set CGO_ENABLED=1

Запустить сборку:
@call go build

В результате сборки получим список ошибок (как в Go, так и в C-коде) или готовый исполняемый файл, который можно запускать на Raspberry.