Я пытаюсь вернуть структуру из общей библиотеки, написанной на C. Это простой код для тестирования возвращаемой структуры и простого int32, libstruct.c
, скомпилированного gcc -shared -Wl,-soname,libstruct.so.1 -o libstruct.so.1 libstruct.c
:
#include <stdint.h>
int32_t newint(int32_t arg) {
return arg;
}
struct MyStruct {
int32_t member;
};
struct MyStruct newstruct(int32_t arg) {
struct MyStruct myStruct;
myStruct.member = arg;
return(myStruct);
}
Я могу использовать эту библиотеку с простой программой C, usestruct.c
, скомпилированной gcc -o usestruct usestruct.c ./libstruct.so.1
:
#include <stdio.h>
#include <stdint.h>
struct MyStruct {
int32_t member;
};
extern struct MyStruct newstruct(int32_t);
extern int32_t newint(int32_t);
int main() {
printf("%d\n", newint(42));
struct MyStruct myStruct;
myStruct = newstruct(42);
printf("%d\n", myStruct.member);
return 0;
}
Я могу запустить его с помощью LD_LIBRARY_PATH=./ ./usestruct
, и он работает правильно, печатает два значения. Теперь напишем аналогичную программу на раку, usestruct.raku
:
#!/bin/env raku
use NativeCall;
sub newint(int32) returns int32 is native('./libstruct.so.1') { * }
say newint(42);
class MyStruct is repr('CStruct') {
has int32 $.member;
}
sub newstruct(int32) returns MyStruct is native('./libstruct.so.1') { * }
say newstruct(42).member;
Это сначала печатает 42
, но затем завершается с ошибкой сегментации.
На C этот пример работает, но я не спец в C, может что-то забыл, какие-то параметры компиляции? Или это баг ракудо?
Интерфейс NativeCall требует, чтобы транзакции C-структур выполнялись с указателями:
Объекты CStruct передаются собственным функциям по ссылке, и собственные функции также должны возвращать объекты CStruct по ссылке.
Однако ваша функция C возвращает новую структуру по значению. Затем, я думаю, это пытаются интерпретировать как адрес памяти, так как он ожидает указатель и пытается читать/записывать из диких областей памяти, отсюда и segfault.
Вы можете указать свою функцию как:
struct MyStruct* newstruct(int32_t val) {
/* dynamically allocating now */
struct MyStruct *stru = malloc(sizeof *stru);
stru->member = val;
return stru;
}
с #include <stdlib.h>
в самом верху для malloc
. Программа Raku, по сути, такая же по модулю некоторой эстетики:
# prog.raku
use NativeCall;
my constant LIB = "./libstruct.so";
class MyStruct is repr("CStruct") {
has int32 $.member;
}
# C bridge
sub newint(int32) returns int32 is native(LIB) { * }
sub newstruct(int32) returns MyStruct is native(LIB) { * }
say newint(42);
my $s := newstruct(84);
say $s;
say $s.member;
Мы собираем библиотеку и запускаем программу Raku, чтобы получить
$ gcc -Wall -Wextra -pedantic -shared -o libstruct.so -fPIC mod_struct.c
$ raku prog.raku
42
MyStruct.new(member => 84)
84
(взял на себя смелость переименовать файл C в «mod_struct.c»)
Выглядит неплохо. Но есть проблема: теперь, когда динамическое распределение было сделано, возникает ответственность за его возврат. И нам нужно сделать это самостоятельно с C-мостовым фризером:
Когда тип на основе CStruct используется в качестве возвращаемого типа собственной функции, сборщик мусора не управляет памятью.
Так
/* addendum to mod_struct.c */
void free_struct(struct MyStruct* s) {
free(s);
}
Отметив, что, поскольку сама структура не имеет динамических распределений для своих членов (поскольку она имеет только целое число), мы не делали дальнейшего освобождения.
Теперь программа Raku должна знать об этом и использовать это:
# prog.raku
use NativeCall;
my constant LIB = "./libstruct.so";
class MyStruct is repr("CStruct") {
has int32 $.member;
}
# C bridge
sub newint(int32) returns int32 is native(LIB) { * }
sub newstruct(int32) returns MyStruct is native(LIB) { * }
sub free_struct(MyStruct) is native(LIB) { * }; # <-- new!
say newint(42);
my $s := newstruct(84);
say $s;
say $s.member;
# ... after some time
free_struct($s);
say "successfully freed struct";
и вывод следует как
42
MyStruct.new(member => 84)
84
successfully freed struct
Вручную отслеживать объекты MyStruct, чтобы помнить об их освобождении через некоторое время, может быть обременительно; это будет писать C! На уровне Раку у нас уже есть класс, представляющий структуру; то мы можем добавить к нему подметод DESTROY, который освобождает себя всякий раз, когда сборщик мусора сочтет это необходимым:
class MyStruct is repr("CStruct") {
has int32 $.member;
submethod DESTROY {
free_struct(self);
}
}
С этим дополнением не требуются ручные вызовы free_struct
(на самом деле, лучше этого не делать, потому что это может привести к двойному освобождению, что является неопределенным поведением на уровне C).
P.S. ваш основной файл C может быть пересмотрен, например, файл заголовка кажется в порядке, но это выходит за рамки или это был только демонстративный пример, кто знает. В любом случае, спасибо за предоставление MRE и добро пожаловать на сайт.
привет @raiph, спасибо за добрые слова. Я точно не знаю, почему это так. Я могу предположить, хотя :) В C лучше передавать структуры по ссылке, особенно когда они большие, поскольку передача по значению копирует все это целиком, и не только теряется скорость, но и стек функции может переполниться. С указателем оба эти вопроса больше не являются проблемой. Тем не менее, это делает структуру is rw
на уровне C :) [продолжение]
[продолжение] (это может быть желательным или нежелательным; при желании, еще одно преимущество указателей!). Учитывая, что вы, вероятно, также являетесь автором части C и/или уже легко ошибиться в C, я думаю, что выбор указателя - это круто. Но опять же, разработчики интерфейса NativeCall и те, у кого больше знаний C, знают лучше меня.
@MustafaAydın, я заметил, что вы используете привязку: my $s := newstruct(84);
. Избегает ли это ненужного копирования возвращаемой структуры?
@fingolfin Извините, надо было это уточнить! Ответ: не совсем. С :=
вместо =
я избегаю обертывания контейнера Scalar вокруг значения RHS (в данном случае экземпляра MyStruct). Это (:=
) заставит $s
напрямую "смотреть" на значение RHS, так сказать, и предотвратит переназначение $s
(например, $s = -7
завершится ошибкой после этого момента; так что своего рода навязанная неизменность, завещание). Вы можете посмотреть о скалярах и контейнерах здесь и здесь. (Также: ваш ответ превосходен, спасибо за это!)
В дополнение к отличному ответу @Mustafa.
Я нашел другой способ решить свою проблему: мы можем выделить структуру в raku и передать ее функции C. Вот пример, файл mod_struct.c
:
#include <stdint.h>
struct MyStruct {
int32_t member;
};
void writestruct(struct MyStruct *outputStruct, int32_t arg) {
outputStruct->member = arg;
}
Файл usestruct.raku
:
#!/bin/env raku
use NativeCall;
class MyStruct is repr('CStruct') {
has int32 $.member;
}
sub writestruct(MyStruct is rw, int32) is native('./libstruct.so') { * }
my $myStruct = MyStruct.new;
writestruct($myStruct, 42);
say $myStruct.member;
Скомпилируйте и запустите его:
$ gcc -Wall -Wextra -pedantic -shared -o libstruct.so -fPIC mod_struct.c
$ ./usestruct.raku
42
еще раз, спасибо, что поделились этим. Здесь хотелось бы указать на одно предостережение: этот подход отлично работает со структурами для членов, не использующих указатели (например, int и float); однако, если он имеет строку (char *) или массив, созданные Raku значения передаются нормально, но их нельзя безопасно использовать на уровне C без клонирования, потому что ссылка может быть уничтожена на уровне Raku (возможно, даже немедленно!) и тогда C-уровень будет искать запрещенную область в памяти. Для них необходимо динамическое размещение на уровне C.
Например, если вы запустите пример здесь без strdup
, но все остальное то же самое, я иногда получаю "foo is str and 123"... но! Я также получаю «foo is &sayM and 123», т. е. странное чтение памяти и даже иногда ошибку «Неверное завершение строки UTF-8», подразумевая, что переданная строка иногда уже ушла (освобождена), и мы сталкиваемся с undefined поведение, как видно.
Еще раз спасибо за ваши объяснения, @MustafaAydın. Я много раз читал эту страницу документации, но благодаря вам начал в ней разбираться!
"Интерфейс NativeCall требует...". Я едва знаю C и определенно недостаточно, чтобы интуитивно понять, почему любое заданное требование NativeCall является таким, какое оно есть, в частности, можно ли однажды улучшить NativeCall, чтобы уменьшить или устранить любое текущее ограничение. Достаточно ли вы знаете, чтобы прокомментировать, может ли это требование когда-нибудь быть устранено (и если да, то знаете ли вы какие-либо ссылки на то, где это обсуждалось?) Если да, подумайте о том, чтобы добавить это в свой ответ или, по крайней мере, прокомментировать . Ваш ответ кажется мне уже выдающимся, но от этого мне хочется большего!