LINUX.ORG.RU

Функции с переменным числом аргументов: va_arg vs void*

 


0

2

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

int api1( unsigned int id, ... );
int api2( unsigned int id, const void* arg );

В случае api1 предполагается передавать набор аргументов в зависимости от id.

В случае api2 предполагается передавать указатель на некую структуру, которая будет отличаться в зависимости от id и в которой будет виден весь список аргументов.

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

А какой вариант предпочли бы вы? Или может существуют ещё способы?

★★★
Ответ на: комментарий от u5er

Не сомневаюсь, но при выборе языка удобство для меня - это не самый главный пункт ;)

Удобство ещё в том, что если нужно в а-ля Си коде можно использовать: std, …

Вообще сам не использую, так как разработал (и пополняю) своё API, которое весьма удобно.

В целом конечно - «на вкус и цвет, товарищей нет».

Пошучу

Как говорил кирпич подойдя к краю крыши другому кирпичу - «Главное, чтобы человек попался хороший».

anonymous
()
Ответ на: комментарий от u5er

Без сомнения, ты решишь эту задачу с помощью C. И второй вариант безусловно рабочий. Всё дело в том, что будет потом. Насколько это будет читабельно, развиваемо, тестируемо. Вот тут возникают вопросы. И другой язык тут более предпочтителен. При строгой типизации уже при компиляции ты найдешь ошибку, которую в C не нашел бы и она бы выскочила на проде. При явном использовании типов явно видно как добавлять новые типы, при использовании полиморфизма не придется делать страшный switch () из приведения от (void*). Ладно, простите за офтоп.

DenProg
()
Ответ на: комментарий от u5er

я определю список id и соответствующие данные, которые будут передаваться через указатель

Ну так не поступай как писатели printf (хотя там тоже много чего в оправдание можно сказать).

представь, что у printf() были бы заранее прописаны все возможные варианты и каждый вариант имел бы свой прототип

Вот об том и говорю, что надо мух от котлет отделять на месте, а не послезавтра. Делай тайпхинтинг и наследование структур на все типы пересылаемых данных — айди чего-то там, оно и будет опорой, если не хватит — добавь поле с типом сообщения. Не еби мозги.

deep-purple ★★★★★
()
Ответ на: комментарий от anonymous

В целом конечно - «на вкус и цвет, товарищей нет».

Немного офтопа.

А чем использование C++ в качестве а-ля Си плохо?

Например. перевёл freetype и SDL на C++ и всё ok!

anonymous
()
Ответ на: комментарий от u5er

Я знаю содердимое для конкретного id, но я не могу заранее предусмотреть id для всех возможных случаев. Именно в этом вся суть. Если бы я заранее мог знать все возможные варианты, то я бы сделал бы обычный api.

В теле своей функции ты перечисляешь все ID. Или switch/case, или if - else if - else if. Не суть.

А значит ты предусмотрел абсолютно все ID. В теле своей функции, а не навызывающей стороне. В чём разница?

her_s_gory ★★
()
Ответ на: комментарий от deep-purple

Ну так не поступай как писатели printf (хотя там тоже много чего в оправдание можно сказать).

Пошучу

Разработчики printf наверное не знают иных способов.

С использованием метаданных printf более ээфективен и универсален.

Для run-time такой подход - «шик, блеск, красота».

anonymous
()
Ответ на: комментарий от deep-purple

А поди загляни в реализацию ngx_printf и спрячь этот пафос поглубже ))

Смотрел конечно.
Пафоса нет.
Не знают.
Впрочем лишний раз посмотрю исходники (ранее не знали).

anonymous
()
Ответ на: удаленный комментарий

Потому анонимус, если ничего дельного не выдает, идет в жопу. Ну, выдай что-то красивое и по делу — ты же можешь.

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

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

Есть задача - организовать общение между потоками. Одним из возможных решений является RPC(удалённый вызов процедур) - механизм, который позволяет вызвать функцию в другом процессе, передать ему аргумент и получить ответ. Эту штуку я и пытаюсь реализовать.

Представим, что есть 2 сервиса. Один - локальный проигрыватель музыки, второй удалённый, который отвечает за освещение. У каждого из них есть заголовочник со своим набором id и описанием того, что вместе с этим id передаётся (и передаётся ли вообще). Очевидно, что эти наборы будут полностью отличаться друг от друга, т.к. выполняют абсолютно разные задачи.

Допустим, я сделаю так, как ты решил - пропишу все id и структуры в заголовочнике механизма rpc. Что дальше? А дальше появляется условный вася пупкин и говорит: я хочу сделать автозапуск автомобиля, стоящего во дворе, и управление воротами, как мне это сделать через твой rpc? На что я справедливо развожу руками.

Теперь, когда я расписал всё более подробно, спрошу: почему бы мне не поступить так, как писатели printf, если я не знаю в данный момент список того, что будет передаваться при вызове, но об этом будет известно при использовании этого api?

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

В чём разница?

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

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

Тще (тно). Тут ты никак не уйдешь от реализации «появляется условный вася пупкин и говорит: я хочу сделать автозапуск автомобиля, стоящего во дворе, и управление воротами».

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

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

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

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

Ты пытаешься переложить личную головную боль на прослойку

Нет, я пытаюсь переложить всю головную боль на того, кто будет использовать этот механизм. Ибо это в его задачу будет входить определение всех аргументов, которые передаются при конкретном id и написание обёрток при желании. Если это будет вася пупкин с его воротами и автозапуском, то он сам решит, какие id передаёт и принимает его код и какие аргументы при этом используются. Я-то каким боком тут?

u5er ★★★
() автор топика
Ответ на: комментарий от deep-purple

Она используется для вызова функции из одного потока в другом и передачи аргументов. Должна быть возможность передавать произвольный набор аргументов. Какие именно аргументы передаются определяются непосредственно при использовании.

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

Если это голосование, то я за вариант 2. Понятнее, что будет происходить, есть надежда, что будет легче отловить испорченную память при ошибках в интерпретации аргументов.

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

Структуру перед вызовом вы можете один раз заполнить, а потом миллион раз вызвать функцию. Копировать через стек вы будете на каждый вызов.

Некоторые поля структуры могут заполняться константами, в таком случае компилятор может это оптимизировать.

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

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

Представим, что есть 2 сервиса. Один - локальный проигрыватель музыки, второй удалённый, который отвечает за освещение. У каждого из них есть заголовочник со своим набором id и описанием того, что вместе с этим id передаётся (и передаётся ли вообще). Очевидно, что эти наборы будут полностью отличаться друг от друга, т.к. выполняют абсолютно разные задачи.

Понятно, функциональность должна легко расширяться в любую неизвестную заранее сторону да к тому же все это должно работать по сети

В таком случае, если не планируется передавать миллионы событий в секунду, то собственный ASCII-протокол или даже JSON видятся уже не самым худшим решением

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

Код не мой, но идея такая

#include <stdio.h>
#include <string.h>

// Object types
typedef enum {
    TYPE_PERSON = 1,
    TYPE_PRODUCT = 2,
    TYPE_ORDER = 3
} ObjectType;

// Person structure
typedef struct {
    int id;
    char name[32];
    int age;
} Person;

// Product structure  
typedef struct {
    int id;
    char name[32];
    float price;
} Product;

// Header for all serialized objects
typedef struct {
    ObjectType type;
    int version;
    int size;
} ObjectHeader;

// Simple serialization function
unsigned char* serialize_person(Person *p, int *buffer_size) {
    // Calculate total size: header + person data
    int total_size = sizeof(ObjectHeader) + sizeof(Person);
    unsigned char *buffer = malloc(total_size);
    
    // Create header
    ObjectHeader header = {TYPE_PERSON, 1, total_size};
    
    // Copy header to buffer
    memcpy(buffer, &header, sizeof(ObjectHeader));
    
    // Copy person data after header
    memcpy(buffer + sizeof(ObjectHeader), p, sizeof(Person));
    
    *buffer_size = total_size;
    return buffer;
}

// Deserialize with type checking
void* deserialize_object(unsigned char *buffer) {
    ObjectHeader *header = (ObjectHeader*)buffer;
    
    printf("Found object: type=%d, version=%d, size=%d\n", 
           header->type, header->version, header->size);
    
    // Check object type
    switch(header->type) {
        case TYPE_PERSON: {
            Person *p = malloc(sizeof(Person));
            memcpy(p, buffer + sizeof(ObjectHeader), sizeof(Person));
            return p;
        }
        case TYPE_PRODUCT: {
            Product *prod = malloc(sizeof(Product));
            memcpy(prod, buffer + sizeof(ObjectHeader), sizeof(Product));
            return prod;
        }
        default:
            printf("Unknown object type!\n");
            return NULL;
    }
}

int main() {
    // Create a person
    Person person = {1001, "John Doe", 30};
    
    // Serialize person to buffer
    int buffer_size;
    unsigned char *buffer = serialize_person(&person, &buffer_size);
    
    printf("Serialized %d bytes\n", buffer_size);
    
    // Deserialize from buffer
    Person *restored = (Person*)deserialize_object(buffer);
    
    if (restored) {
        printf("Restored: id=%d, name=%s, age=%d\n", 
               restored->id, restored->name, restored->age);
        free(restored);
    }
    
    free(buffer);
    return 0;
}

Пишем некий код стерилизации, как им то известным способом упаковываем данные, а потом их извлекаем. Удобно.

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

Не, это не ко мне. Прочти вот этот пост Функции с переменным числом аргументов: va_arg vs void* (комментарий)

Если такое делать, то этим должен заниматься уровень выше, но не я. Моя задача - организовать вызов и передачу произвольных аргументов «как есть». Как их принимать и обрабатывать стороны «должны решить» заранее.

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

Если я правильно все понял, то тогда вызов должен быть таким

int api3( unsigned int id, const void* arg, const size_t size);

Мы просто передаем в id кусок память, и все, остальное нас не колебет.

Единственное мне не нравится id, люди в числах обязательно запутаются. Если делать все через одну ручку, то лучше для этого взять строку.

Или же делать на каждый id (rpc) свою ручку.

int v1_api_save_track(const void* name, const void* ptr, const size_t size);
int v1_api_del_track(const void* name);
список v1_api_list_tracks();

Второе проще.

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

Единственное мне не нравится id, люди в числах обязательно запутаются.

Поэтому для таких вещей пишется набор дефайнов или enum, где числовые константы «прячутся» за буквами.

const size_t size

Какой смысл использовать тут константу?

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

Поэтому для таких вещей пишется набор дефайнов или enum, где числовые константы «прячутся» за буквами.

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

Какой смысл использовать тут константу?

Это же стандартный способ передать кусок памяти, указатель на начало + его длина.

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

Представь список из 50 или больше дефайн. Цифру перепутать очень просто.

Так пофиг на цифру. Пишешь в заголовке такую штуку

enum{
	CMD_ENABLE_TO,
	CMD_DISABLE_CE,
	CMD_...
};

И всё! Дальше ты забываешь про цифры, а в коде подставляешь сами енумки.

Это же стандартный способ передать кусок памяти, указатель на начало + его длина.

Ты не понял. Я спросил про константу. Например, вот прототип функции
ssize_t write(int fd, const void *buf, size_t count);
Видишь? В ней используется size_t count, а не const size_t count. Я и спрашиваю: почему ты написал const? Я такого ни разу не встречал.

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

Так пофиг на цифру. Пишешь в заголовке такую штуку

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

Ты не понял. Я спросил про константу. Например, вот прототип функции

Я как то привык к такому для C++. Действую уже на автомате. Сейчас не скажу зачем, что-то связанное в переопределение функций.

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

и поэтому они обязательно разойдутся у клиентов.

Неверно. Заголовочник один на всех. Все подключают этот заголовочник и у всех энумки примут одинаковые значения.

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

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

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

И еще проблема, числа идут последовательно к примеру:

enum{
	CMD_TRACK_PLAY,
	CMD_TRACK_STOP,
	CMD_TRACK_DEL,
        // дальше прямо сразу
        CMD_MP3_TAG_ADD,
        CMD_MP3_TAG_DEL,
};

Теперь надо добавить еще одно действие в TRACK, если без перекомпиляции то так

enum{
	CMD_TRACK_PLAY,
	CMD_TRACK_STOP,
	CMD_TRACK_DEL,
        // дальше прямо сразу
        CMD_MP3_TAG_ADD,
        CMD_MP3_TAG_DEL,
        CMD_TRACK_PAUSE,
};

или так, но надо всех пересобирать.

enum{
	CMD_TRACK_PLAY,
	CMD_TRACK_STOP,
	CMD_TRACK_DEL,
        CMD_TRACK_PAUSE,
        // дальше прямо сразу
        CMD_MP3_TAG_ADD,
        CMD_MP3_TAG_DEL,        
};

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

alnkapa
()
Ответ на: комментарий от Vic

Ну вот смотри. Я уже запилил механизм RPC. Получилось следующее. Для отправки вызовов есть такие функции

int rpcCallByServer( struct RpcServerSideConnection* connection, unsigned int callId, const void* callData );
int rpcCallByClient( struct RpcClientSideConnection* connection, unsigned int callId, const void* callData );

А входящие вызовы приходят в функции обратного вызова

int (*onServerCall)( void* clientData, unsigned int callId, const void* callData );
int (*onClientCall)( void* serverData, void* clientTag, unsigned int callId, const void* callData );

Не особо сложно-то и получилось. И я тебе больше скажу, передача аргументов - это была самая лёгкая задача из тех, которые мне пришлось решить по пути ;)

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

Теперь надо добавить еще одно действие в TRACK, если без перекомпиляции то так

Ты на приколе что ли? Ты добавляешь новую команду. Стало быть, ты меняешь API. Какое, нахрен, без перекомпиляции? Перекомпиляция обязательна для всех, если заголовочник изменился.

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

Почему она обязательна то? Зачем? Вот представь твоя прога живет на мобиле например. Какова вероятность что, клиент ее обновит даже если ему об этом сказать. По опыту это ситуация близкая к нулю. Так что ни разу не шучу.

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

Почему она обязательна то? Зачем?

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

Какова вероятность что, клиент ее обновит даже если ему об этом сказать. По опыту это ситуация близкая к нулю. Так что ни разу не шучу.

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

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

И я тебе больше скажу, передача аргументов - это была самая лёгкая задача из тех, которые мне пришлось решить по пути ;)

Я знаю. Путь тебе предстоит увлекательный.

Обмен сообщениями-то уже есть? Ибо под капотом любого RPC лежит обмен сообщениями.

typedef struct
{
    ExMsgType_t MsgType; // Тип сообщения.
    int Tag; // Произвольный тэг.
    void* Data; // Данные, размещенные в динамической памяти.
    int DataSize; // Размер данных.
} ExMsg_t;
Vic
()
Ответ на: комментарий от Vic

Обмен сообщениями-то уже есть?

Пока нет. У меня пока что получился LPC, но вроде работает - вызовы проходят, аргументы передаются, возвращаемое значение возвращается. Сообщения будут в будущем, когда руки дойдут именно до RPC.

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

Синхронный и асинхронный варианты обмена сообщениями и RPC, различаешь? Какой вариант будешь делать? (влияет на код и архитектуру программы клиента)

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

Синхронный. Без вариантов. Это очень удобно для запросов. Например, я делаю вызов на сервер и сервер уже решает, блокируется мой вызов или нет. Зависит от логики на сервере. Сервер может у себя «поставить галочку» и вернуть мне управление сразу, а может выполнить мой запрос в этом вызове.

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

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

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

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

Асинхронный вариант пилить вообще не вижу смысла

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

Не проще сразу придумать асинхронный RPC, который по сути обмен сообщениями, и клиент будет сам проверять наличие ответа в подходящий клиенту момент, не дергая сервер на предмет готовности задания (которое еще надо будет как-то идентифицировать отдельно от клиента и сервера) ?

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

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

Сервера же пишут для однотипной обработки RPC запросов от множества клиентов и там время передачи и загруженность сервера будет иметь значение.

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