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 и в которой будет виден весь список аргументов.

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

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

★★★

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

Реализовывал функции с вариантивными аргументами всего несколько раз, без манов и тыкания палочкой не смогу сразу нормально написать :)

Bfgeshka ★★★★★
()

Переменный список аргументов хорош для функций типа printf, когда строка формата парсится в момент выполнения. У вас так вообще может быть третий вариант — массив указателей на функции :)

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

массив указателей на функции

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

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

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

Цена вопроса - размер указателя.

а так же размер структуры т.к просто аргументы могли бы уместиться в регистры в зависимости от calling conversion

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

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

struct args3 s;
s.a = 1;
s.b = 2;
call_by_index(3, &s);
вместо
call_by_index(3, 1, 2);
это мазохизм.

А вообще такие индексированные вызовы без веских причин делать не надо.

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

а так же размер структуры

Не уверен. Структура - это набор переменных, которые были бы переданны в функцию в качестве аргументов путём копирования. А если я передаю указатель на структуру, то эти аргументы передаются не через стек и/или регистры, а лежат где-то в памяти, а вместо них передаётся указатель.

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

POSIX функция open работает именно как в варианте 1. В libcurl функция curl_easy_setopt объявлена так же, хотя и рассчитана только на один дополнительный аргумент.

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

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

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

А разве есть особая разница между раскладыванием аргументов по полям структуры и раскладыванием аргументов при вызове функции?

А вообще такие индексированные вызовы без веских причин делать не надо.

Реализация RPC - это достаточно веская причина?

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

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

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

А разве есть особая разница между раскладыванием аргументов по полям структуры и раскладыванием аргументов при вызове функции?

А ты сам не видишь? В 4 раза больше строк и одна лишняя переменная.

Реализация RPC - это достаточно веская причина?

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

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

А ты сам не видишь? В 4 раза больше строк и одна лишняя переменная.

И какая разница? Ты всё равно всё в одну строчку запишешь, я тебя знаю xD

RPC слишком общее понятие

Пока что это будет LPC, то есть работать на одной машине, но кто знает… Мне нужен механизм, чтоб из одного потока запустить функцию в контексте другого потока и передать ему набор заранее определённых аргументов. Вот сейчас и стою как осёл между двумя стогами сена. Как обычно…

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

И какая разница? Ты всё равно всё в одну строчку запишешь, я тебя знаю xD

Даже если так, эта строчка получится длиннее в несколько раз и некрасиво выглядящей. Вот с теми же сисколлами прекрасный пример: есть низкоуровневый способ запустить сисколл по индексу, а есть libc-обёртки с удобной передачей аргументов.

Мне нужен механизм, чтоб из одного потока запустить функцию в контексте другого потока и передать ему набор заранее определённых аргументов.

Ну, пересылать информацию о вызове между тредами придётся структурой. А дальше как уже писал: можешь сделать красивые обёртки ко всему. Можешь и не делать. Способ со структурой (поскольку он нативный) будет в любом случае, вопрос только в том будет ли он единственным или нет.

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

Есть и 3-й вариант. Если по id всё равно switch’ить – то может быть сразу объявить X функций с конкретными аргументами?

т.е. вместо

int apifunc(int id, void *arg) {
    switch (id) {
        case 0:
             (struct data0*)arg;
             break;
        case 1:
             (struct data1*)arg;
             break;
        // и т.д.
    }
}

не морочить голову и сразу сделать

int apifunc0(int a, int b);
int apifunc1(char *x, int y);
int apifuncX(struct ku*);
// …
beastie ★★★★★
()
Ответ на: комментарий от u5er

Так все равно придется предусмотреть. Магии нет. Или ты знаешь размер и содержимое void* по id, или у тебя есть понимание что передается в списке аргументов (как в printf - на основе анализа строки).

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

Или ты знаешь размер и содержимое void* по id

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

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

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

А какой вариант предпочли бы вы?

Второй. В ядре довольно часто встречается

	/* Initialize the spi_controller fields */
	host->num_chipselect = 4;
	host->mode_bits = SPI_TX_DUAL | SPI_TX_QUAD | SPI_RX_DUAL | SPI_RX_QUAD;
	host->flags = SPI_CONTROLLER_HALF_DUPLEX;
	host->max_speed_hz = AMD_SPI_MAX_HZ;
	host->min_speed_hz = AMD_SPI_MIN_HZ;
	host->setup = amd_spi_host_setup;
	host->transfer_one_message = amd_spi_host_transfer;
	host->mem_ops = &amd_spi_mem_ops;
	host->mem_caps = &amd_spi_mem_caps;
	host->max_transfer_size = amd_spi_max_transfer_size;
	host->max_message_size = amd_spi_max_transfer_size;

	/* Register the controller with SPI framework */
	err = devm_spi_register_controller(dev, host);

или даже

static struct platform_driver amd_spi_driver = {
	.driver = {
		.name = "amd_spi",
		.acpi_match_table = ACPI_PTR(spi_acpi_match),
	},
	.probe = amd_spi_probe,
};

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

Или может существуют ещё способы?

JSON 🙂

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

Я тут подумал и пришёл к выводу, что такое заявление спорное. Во-первых, перед вызовом всё равно структуру заполнять, а во-вторых, доступ по указателю медленнее. Так что нельзя прям уж так однозначно заявить.

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

Я бы предпочёл второй вариант из-за типобезопасности (тебе надо лишь использовать правильную структуру, ты не можешь передать не тот тип не в той позиции и т. п.). Также это оставляет потенциал для расширения структуры в будущем (добавление полей в конец сохраняет совместимость с предыдущими версиями структуры, а по каким-нибудь флагам внутри структуры можно определять её версию). Такую функцию проще дёргать из других языков программирования по FFI. Функции с переменным числом аргументов вообще лучше наружу не высовывать, они больше сахар для внутренних вызовов, чтобы не объявлять переменные на каждый чих (но для внешнего API ничто не мешает написать обёрток под каждую версию вызова) или где набор аргументов вообще уникален для каждого вызова (например, как у printf).

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

Если передаётся много параметров («много» на многих архитектурах примерно больше 6 чисел, в примерах ядерного кода параметров явно больше), то они в любом случае в регистры не влезут и пойдут в стек. А стек находится в ОЗУ так же как и твоя структура. Более того, это буквально та же самая память, ведь такие структуры как правило создают на стеке, а не в куче (та же самая локальность памяти, кеши и т. п.). Просто вместо регистра указателя стека адрес структуры берётся из какого-то другого регистра общего назначения (первые N параметров на современных архитектурах идут в регистрах). В большинстве случаев это будет один-в-один по тактам.

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

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

Тебе всё равно определять соотношение id → struct.

Добавить struct+switch case или добавить новую функцию – это одно и тоже. Только в случае с функций у тебя хотя бы какая-то гарантия типов.

Т.ч.

  1. набор функций – наибольшая безопасность типов аргументов
  2. *void – шлём что получится и как получится и ловим sigfault на каждый чих
  3. va_args – тут без дополнительных определений, что там будет в аргументах, полный абзац
beastie ★★★★★
()
Последнее исправление: beastie (всего исправлений: 1)

Для варианта со структурой можно добавить третий аргумент - размер структуры. Это позволит расширять api добавлением новых полей.

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

В 4 раза больше строк и одна лишняя переменная.

С составными литералами нет необходимости в дополнительной переменной

call_by_index(3, &(struct args3) {1, 2});
No ★★
()

Цена вопроса - размер указателя.

Чё-то не понял, в x32 4 байта, в x64 8 байт, в любом случае структуру передавать всегда удобнее И id можно внутрь структуры поместить, тогда у тебя ф-ция вообще один параметр будет иметь

IvanRia
()