2 заметки с тегом

cgo

Структуры в Си и CGO

По горячим следам хочу написать какая забытая мною особенность языка Си вчера ввела меня в заблуждение и почему несмотря на то, что я работал с сишной структурой, CGO так как будто не считало.

Для начала посмотрим как определяются и передаются структуры в Си. Полную спецификацию я приводить не буду, посмотрим на два распространённых способа. Первый используется в этом небольшом примере:

#include <stdio.h>
#include <math.h>

struct point {
	double x, y;
};

double point(struct point p) {
	return sqrt(pow(p.x, 2) + pow(p.y, 2));
}

int main() {
	printf("%g\n", point((struct point){1, 2}));
}

Тут можно обратить внимание на две особенности — во-первых, везде, где мы используем структуру, приходится использовать и ключевое слово struct, во-вторых, в коде существует и структура point, и функция с таким названием.

Это происходит потому, что имена структур обитают в своём собственном пространстве имён, на его использование и указывает struct. Там же живут имена объединений (union) и перечислений (enum). Имена функций находятся в общем пространстве, так что два таких определения с одним именем друг другу не мешают.

Структуры, объявленные таким образом видны через CGO компилятора Гоу как C.struct_name (C.struct_point в данном случае). К сожалению, тут есть особенность, о которую я споткнулся — если запрашиваемая структура не определена, то Гоу не выдаст ошибки, мы просто получим пустую структуру:

package main

import ("C"; . "fmt")

func main() {
	Println(C.struct_undefined{}) // ошибки не будет
}

Но программисты — люди ленивые, писать всюду struct неудобно. К счастью есть способ этого не делать — можно завести структуру, а потом дать ей имя в пространстве всех остальных типов. Это делается при помощи ключевого слова typedef позволяющего создавать алиасы типов:

struct point {
	double x, y;
};

typedef struct point point;

Теперь у нас есть два имени для одного типа — один в пространстве структур и прочего, второе — в общем пространстве. Можно даже сделать их одинаковыми, что очень удобно. Если второе имя нам ни к чему (бывают случаи, когда оно необходимо, но я не буду их тут рассматривать), то можно определить анонимную структуру и сразу дать имя этому типу в общем пространстве, так тоже будет работать.

Это нас подводит ко второму способу работы со структурами:

#include <stdio.h>
#include <math.h>

typedef struct {
	double x, y;
} point;

double veclen(point p) {
	return sqrt(pow(p.x, 2) + pow(p.y, 2));
}

int main() {
	printf("%g\n", veclen((point){1, 2}));
}

Ключевое слово struct перед типом уже не нужно, раз все имена в общем пространстве. Это же касается и CGO.

Как раз на эти грабли я и наступил — позабыл про эти все особенности. Когда я ещё писал на Си, всегда пользовался вторым способом, с именем в общем пространстве и привык к тому, что это и есть структуры.

Структуры и CGO

Обновление: в комментариях в Фейсбуке мне бывший коллега попенял, что я забыл Си и что поведение правильное — тут объявляется анонимная структура, а потом к ней определяется тип. Верно, практики на Си у меня за последние годы почти не было, вымылось всё из памяти. Так что текст ниже читайте с учётом этого обновления заметки.

Каждый раз расстраиваюсь, когда сталкиваюсь с CGO. Идея-то прекрасная — встраивать в программы на Гоу сишные библиотеки почти без программирования на Си. Но частности всё убивают. То нельзя функцию по указателю передать, то вот убил час с утра сегодня, пытаясь починить ошибку в своей библиотеке go-gd.

Документация говорит нам, что сишные структуры доступны в Гоу через конструкцию C.struct_name, а её размер — через C.sizeof_struct_name.

Я так и пытался сделать и получал ошибки:

package main

/*
typedef struct {
    int x, y;
} test;
*/
import "C"
import . "fmt"

func main() {
    Println(C.struct_test{}) // «{}»? Что за…
    Println(C.sizeof_struct_test) // could not determine kind of name for C.sizeof_struct_test
}

Упрощённый код выше кратко иллюстрирует проблему. В первом случае никакая ошибка не выводится (что ещё больше сбивает с толку), но структура почему-то пустая. Во втором случае компилятор выдаёт ошибку, но от этого не легче — при конфликте моих знаний с документацией как правило думаю, что права документация, потому около часа пытался понять где я тут умудрился накосячить.

Оказывается, и я набрёл на это случайно, в ходе экспериментов, struct просто не надо писать, тогда всё будет работать:

Println(C.test{}) // «{0 0}»
Println(C.sizeof_test) // 8

Если будет время, ещё поизучаю этот вопрос — посмотрю историю коммитов и всё такое, возможно была какая-то движуха, которую не отразили в документации, либо просто что-то сломали. Надеюсь кому-нибудь сэкономлю время этой заметкой.