LINUX.ORG.RU

Binary compatibility в Си

 ,


0

1

Привет! Допустим, есть библиотека с функцией, которая возвращает объект структуры в стеке. Например

struct Data
{
    int size;
    void *data;
};

struct Data getData();


В новой версии библиотеки я хочу расширить структуру Data, но сохранив бинарную совместимость. На ум приходит только добавление пустого массива фиксированной длины в изначальную структуру, забирая оттуда элементы по мере добавления новых полей в структуру:

/* VER 1 */
struct Data
{
    int32_t size;
    void *data;

    char reserved[32];
};


/* VER 2 */
struct Data
{
    int32_t size;
    void *data;

    int32_t new_data;
    char reserved[28]; /* -4 */
};


Однако и тут нужно думать о выравнивании, т.к. надо точно знать сколько забирает в байтах новое поле структуры...

Как есть ещё мысли как обеспечить бинарную совместимость?

★★★

Последнее исправление: former_anonymous (всего исправлений: 2)

  1. union Data { struct Data1 data; int reserved[32]; };

  2. struct Data { struct Data1 data; char reserved[128 - sizeof(struct Data1)]; };

  3. struct Data0
    {
        int32_t size;
        void *data;
    
        int32_t new_data;
    };
    
    
    struct Data
    {
        int32_t size;
        void *data;
    
        int32_t new_data;
        char reserved[32 - sizeof(struct Data0)];
    };
    

    Data0 отличается от Data только наличием поля reserved.

NeXTSTEP ★★
()
Последнее исправление: NeXTSTEP (всего исправлений: 1)
/* v1 */

typedef struct
{
    int32_t size;
    void *data;
}
v1_data_t;

v1_data_t v1_get_data();

/* v2 */

typedef struct
{
    v1_data_t head;
    int32_t flags;    
}
v2_data_t;

v2_data_t v2_get_data();
Serral
()

А если структуры передавать не по значению, а по указателю?

P.S.: Я сишник не настоящий.

te111011010
()
Последнее исправление: te111011010 (всего исправлений: 1)

Почему бы не воспользоваться вариантом:

struct Data
{
    int32_t size;
    void *data;

    struct Data_impl *impl;
};

И уже в impl складывать добавленные данные?

andalevor ★★
()
Ответ на: комментарий от NeXTSTEP

В union стоит добавить long long, чтобы гарантировать максимальное выравнивание.

yugr
()

Иногда есть смысл добавить сериализацию (например msgpack, есть сишная реализация), если вызов функции «тяжелый» и затраты на сериализацию будут сравнительно малы. Зато это даст более качественный контроль над версиями плюс можно реализовать взаимодействие разных версий кода. Сложность возрастает, конечно, но если нужна поддержка разных версий от этой сложности трудно будет избавиться.

yetanother ★★
()

помнится, в первых opengl была фишка с версиями структур. они там в структуру засунули хвост с полем size, в котором хранился sizeof самой структуры, а после него - все прочие поля. таким образом они разграничили версии структур, чтобы определять, какую именно версию использовать.

но вообще, обычно переменные структуры делаются чем-то вроде

struct {
    int a;
    float b;

    char *data;
};

и дальше ты просто выделяешь место динамически, а data будет указывать на начало дополнительных данных.

Iron_Bug ★★★★★
()
Ответ на: комментарий от Iron_Bug

получится не очень-то, потому что к этим данным пользователь должен иметь нормальный доступ. Иначе нет смысла их вообще возвращать. Это не PIMPL, это именно данные, которые нужны пользователю. Просто от версии к версии их набор может увеличиваться.

former_anonymous ★★★
() автор топика
Ответ на: комментарий от former_anonymous

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

Iron_Bug ★★★★★
()

Возвращать объект другого размера в любом случае не выйдет, так что сценарий с уменьшающимся полем reserved - единственный. С выравниванием поможет offsetof.

Если заполнять структуру, переданную по ссылке, то свободы чуть больше, так в win32 api сделано, где в куче структур первое поле указывает размер структуры, а заполняющий код обрабатывает это, чтобы не записать лишнего.

roof ★★
()
struct 
{
    int struct_ver;
    ...
};

или же, как у майкрософта:

struct 
{
    int struct_size;
    ...
};
lovesan ★★
()
Последнее исправление: lovesan (всего исправлений: 1)
Ответ на: комментарий от roof

Хм, глупость написал. По карйней мере, напрямую offsetof не получится использовать. Надо подумать.

roof ★★
()
Ответ на: комментарий от roof

Если заполнять структуру, переданную по ссылке, то свободы чуть больше, так в win32 api сделано, где в куче структур первое поле указывает размер структуры, а заполняющий код обрабатывает это, чтобы не записать лишнего.

Это вариант, но тут есть подлянка в виде заполнения дырок в структуре и опять же выравнивания всей структуры. Если я добавлю одно поле, то запросто может получится, что размер новой структуры остался таким же. В MS с этим проще, т.к. разные версии структур различаются очень значительно, и один размер иметь не могут.

former_anonymous ★★★
() автор топика
Ответ на: комментарий от former_anonymous

Что-то я не очень понимаю, как добавление поля в конец может не изменить размер структуры. Компилятор же не переупорядочивает поля структуры. Если где-то была дырка из-за выравнивания, она останется и после расширения. Главное всегда добавлять поля только в конец структуры и никогда ничего не удалять.

roof ★★
()
Ответ на: комментарий от former_anonymous

Ух ты, что-то новенькое. Зачем конец (я так понимаю, размер) структуры может быть выровнен? Где про это почитать? Беглый гуглинг ничего не дал, и я про такое вообще не слышал никогда.

roof ★★
()

Однако и тут нужно думать о выравнивании

Если ты хлопочешь про ABI, то должен явно и задать выравнивание и порядок байт. Иначе у тебя без всяких версий будут разные структуры на разных машинах.

char reserved

Так ты просто сделай char[SIZE] и в нём уже структуру. Ну т.е. функции будут принимать этот opaque массив (можешь его тайпдефнуть, если хочется) и кастовать его в структуру.

no-such-file ★★★★★
()
Последнее исправление: no-such-file (всего исправлений: 1)
Ответ на: комментарий от roof
#include <stddef.h>
#include <stdio.h>

struct A
{
    float f;
    char c;
};

struct B
{
    float f;
    char c1;
    char c2;
};

int main(int argc, char *argv[])
{
    printf("%ld %ld\n", sizeof(struct A), sizeof(struct B));
    printf("A.f: %ld A.c: %ld\n", offsetof(struct A, f), offsetof(struct A, c));
    printf("B.f: %ld B.c1: %ld B.c2: %ld\n", offsetof(struct B, f), offsetof(struct B, c1), offsetof(struct B, c2));
    return 0;
}




8 8
A.f: 0 A.c: 4
B.f: 0 B.c1: 4 B.c2: 5



Обрати внимание, что смещение c2 - 5. А размер структуры - 8.

former_anonymous ★★★
() автор топика
Ответ на: комментарий от no-such-file

Если ты хлопочешь про ABI, то должен явно и задать выравнивание и порядок байт

Кажется я такое видел в структурах ядра. Там выравнивание иногда делается руками (созданием неиспользуемых полей в структуре) на какую-то определённую границу. Не знаю насколько это вообще надёжно для задач user-space.

former_anonymous ★★★
() автор топика
Последнее исправление: former_anonymous (всего исправлений: 2)
Ответ на: комментарий от former_anonymous

Круто. Интересно, как я умудрился это упустить. Спасибо. Тогда, действительно, не размер, а версия нужна в первом поле.

roof ★★
()
Последнее исправление: roof (всего исправлений: 1)
Ответ на: комментарий от roof

размер от версии ничем не отличается, в данном случае. можно для удобства написать функцию, которая будет возвращать «версию». но размер хранить удобно чисто в утилитарном, программистском плане.

Iron_Bug ★★★★★
()
Ответ на: комментарий от roof

тогда бинарной совместимости не будет в принципе. задача-то в том, чтобы и старый софт мог работать с данными.

Iron_Bug ★★★★★
()
Ответ на: комментарий от Iron_Bug

Почему? Старый софт проверяет, что версия не ниже той, с которой он собран и работает с теми данными, про которые знает.

roof ★★
()
Ответ на: комментарий от roof

у тебя будут разные данные для разных версий. не дополненные, а *разные*. и это нарушает совместимость.

в норме весь софт, который подразумевает расширение структур данных, просто дописывает новые поля в хвост структуры. тогда и совместимость есть и реализация работы с данными очень простая.

Iron_Bug ★★★★★
()
Последнее исправление: Iron_Bug (всего исправлений: 1)
Ответ на: комментарий от Serral

на самом деле, это пока что видится мне самым лучшим вариантом. Но в этом случае будем иметь несколько отдельных деклараций структур. Хотя, может это и самое лучшее и надёжное решение без compiler-specific вещей. То есть универсальное и 100% переносимое.

former_anonymous ★★★
() автор топика
Ответ на: комментарий от roof

нет, тупо копируем структуры. Типа того


struct A_v1
{
    float f;
};

struct A_v2
{
    float f;
    char c;
};

struct A_v3
{
    float f;
    char c;
    int data;
};

A_v1 get_data_v1();
A_v2 get_data_v2();
A_v3 get_data_v3();

/* Simple alias */
#define get_data get_data_v3
typedef struct A_v3 A;


Не очень красиво, но 100% ABI совместимо и переносимо. Даже больше - с этим решением можно в новых версиях переупорядочивать поля ничего не сломав.

former_anonymous ★★★
() автор топика
Последнее исправление: former_anonymous (всего исправлений: 2)
Ответ на: комментарий от roof

это не бинарная совместимость. короче говоря, сам просвещайся. мне лень пространно объяснять. бинарная совместимость, это когда у тебя уже собранный ранее софт работает с новыми библиотеками, источниками данных и т.д.

причём вниз совместимости нет, для этого и нужен контроль размера структуры (ну или «версия»).

Iron_Bug ★★★★★
()
Последнее исправление: Iron_Bug (всего исправлений: 1)
Ответ на: комментарий от roof

Вот здесь в конец структуры добавлено поле, а размер не изменился

Это опасная игра, потому что в какой-то момент на какой-то машине может оказаться, что размер изменится. Для этого нужно явно объявлять align или packed, если нужно предсказуемый размер структуры.

byko3y ★★★★
()
Ответ на: комментарий от Iron_Bug

Кажется, я понял, о чем ты.

Действительно, @former_anonymous, ведь от того, что размер не изменился, хуже не будет. Ты все еще можешь обновить библиотеку не пересобирая пользовательский код. Старый код не будет знать про новые поля и не будет их использовать. Новый код будет знать про них и будет их использовать. Все будет работать. А для того, чтобы новому коду не подсунули старую библиотеку, можно отдельный вызов предумотреть, с проверкой версии один раз, если такой сценарий возможен.

roof ★★
()
Ответ на: комментарий от roof

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

Iron_Bug ★★★★★
()
Ответ на: комментарий от roof

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

former_anonymous ★★★
() автор топика
Ответ на: комментарий от former_anonymous

Вложенные структуры с версиями. Жесть же, нет? Может, не надо так усложнять? Не боишься превратить код своей библиотеки в адскую лапшу?

roof ★★
()
Ответ на: комментарий от roof

у вложенных структур не будет версий, версия будет только одна, самая главная структура будет версионироваться. Если данные меняются нечасто - то это вариант. По крайней мере для меня. Скажем, 5 версий - это я считаю нормально. Опять же, прелесть в том, что с помощью макросов можно избежать этого вырвиглазия с _v1, и для пользователя библиотеки всё будет просто, прозрачно, и обратно совместимо.

former_anonymous ★★★
() автор топика

У aggregate return нет бинарной совместимости. Передавай по указателю

mittorn ★★★★★
()

Бинарную совместимость между чем и чем? Это на диск или в сеть отправится? Если нет, то не проще ли софт пересобрать в тех редких случаях, когда ты структуру меняешь?

kirk_johnson ★☆
()
Последнее исправление: kirk_johnson (всего исправлений: 4)

Допустим, есть библиотека с функцией, которая возвращает объект структуры в стеке.

А ты так не делай и все. Но если уж хочешь, то void *private; где будет ссылка на структуру с приватными полями, так в gobject и делается.

insw
()
Ответ на: комментарий от t184256

хотя бы на порядок полей

ЕМНИП это требуется стандартом.

no-such-file ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.