avva: (Default)
[personal profile] avva

Нижеследующее будет интересно только программистам.

Время от времени, когда мне приходится сталкиваться с тем, как в Перле или других похожих языках устроен интерфейс с библиотеками (shared libraries - .so на юниксах и DLL на Windows), их безумие меня раздражает. В Перле, например, вся система XS для написания расширений на C - одно сплошное упражнение в прикладном мазохизме.

Как правило, приходится писать на C с использованием кучи заголовков и макросов данного языка; потом все это компилируется в shared library, которую может загрузить программа на данном языке во время выполнения, динамически. Зачастую перед компиляцией нужно еще исходники пропустить через специальный препроцессор для данного языка или данной интерфейсной библиотеки (см. SWIG), у этого препроцессора есть свой синтаксис, который нужно помнить, итд. Иногда все это выглядит очень просто - например, модуль Inline в Перле позволяет вызывать код на C прямо так, без казалось бы всякого клея, оно берет и работает - но на самом деле там за кулисами происходит, во время исполнения, создание XS-модуля, компиляция его, и кэширование результата.

Главное, что остается непонятным - зачем вообще нужно что-то компилировать. Мы хотим загрузить файл foobar.dll, в котором есть функция foo(), и после этого вызвать ее из нашего относительно-динамического языка. Мы знаем ее прототип. Мы знаем соглашение вызова (calling convention), которое она использует. У нас есть вся информация, чтобы прямо во время исполнения сконструировать правильный вызов к этой функции. Нам не нужен компилятор C, чтобы сделать эту работу для нас. Да, конечно, с компилятором в целом проще и выглядит надежнее, но какой же это, с другой стороны, overkill; и необходимость правильной трансляции между типами нашего языка и типами C приводит к необходимости использовать ужасные препроцессоры, макросы, и прочую дрянь.

Большинство функций, которые мы хотим вызвать таким образом - простые. Они получают в качестве аргументов такие простые вещи, как int или char* и возвращают то же. Иногда они что-то пишут в один из аргументов по указателю. Для всего этого сделать правильный перевод аргументов туда-обратно прямо во время исполнения - тривиально и не занимает много времени. Для более сложных случаев, когда в фунцкию надо передать, скажем, указатель на структуру, можно придумывать более сложные решения. Скажем, компактное описание структур; или плюнуть и вернуться к компилятору. Но для самых простых случаев, которых очень много - зачем? Мне кажется идиотским тот факт, что вот у меня лежит какая-нибудь libgtk-1.2.so.0 или kernel32.dll, в ней лежит какая-то функция, которая мне нужна, и принимает примитивную строку, и возвращает примитивное число, и я все знаю для того, чтобы ее вызвать, но нет, мне надо идти и писать какую-то хрень на промежуточном клейном языке, чтобы это скомпилировалось, вызвалось, и уже в свою очередь вызвало эту мою функцию.

Пару дней назад опять об этом подумал, на этот раз в применении к языку Lua. Он уж, казалось бы, просто создан для соединения с кодом на C, все для этого заточено, но все равно - вызывать можно только функцию, объявленную определенным образом, с определенными аргументами. Хочешь вызвать какую-то хорошо известную функцию из хорошо известной системной библиотеки - будь добр написать и скомпилировать абсолютно бессмысленный промежуточный клей. Глупо.

Сегодня рассказал об этом своем раздражении Галю, и решил заодно еще раз поискать, есть ли где нормальное решение - и оказалось, что есть в Python: библиотека ctypes, начиная с последней версии языка стандартная. Она делает все именно как я описываю: во время исполнения, без компиляции. Я не знаю Python (пока что - собираюсь вскоре изучить), и не хочу прямо сейчас им начинать пользоваться, но почему там есть, а в том же Перле или Lua - нет?

В общем, решил, что спасение утопающих - дело рук самих утопающих, и начал писать соответствующий модуль (для Perl) вместе с [livejournal.com profile] gaal'ем сегодня. Версия 0.01 будет вызывать только по соглашению cdecl, работать только на x86 linux, и предполагать, что аргументы бывают только 32-битные. Зато этот примитивный прототип уже почти закончен; затем добавим stdcall и соответственно поддержку Windows, а также 64-битные машины и 64-битные аргументы. В более отдаленных планах - parsing прототипа функции в рантайме, правильная обработка передачи буферов и возврата в них результатов, обработка структур с помощью pack/unpack итд.

Ненавижу бессмысленный клей.

Date: 2007-01-31 10:00 pm (UTC)
From: [identity profile] ex-svpv.livejournal.com
Клей не бессмысленный, клей можно компилировать с -Wall -Werror, получая все статические проверки и т.п. А если делать просто dlopen/dlsym и дальше вызывать функцию по адресу, то весь calling convention держится на честном слове. При этом хорошо если функция и сама библиотека действительно stateless, а ведь в некоторых случаях нужно не забыть вызвать какой-нибудь SSL_library_init() где-нибудь в BOOT.

Препроцессор недоступен. Многие вещи по части совместимости разных версий решаются на уровне препоцессора. Вот пример:

- if (rpmReadPackageHeader(Fd, &h, &isSource, Null(int *), Null(int *))) {
+#if RPM_VERSION >= 410
+ if (rpmReadHeader(NULL, Fd, &h, NULL))
+#else
+ if (rpmReadPackageHeader(Fd, &h, &isSource, Null(int *), Null(int *)))
+#endif
+ {

К тому же so библиотека имеет soname, открывать ее просто так через симлинк /usr/lib/lib$name.so это.... Нужно открывать через /usr/lib/$soname, причем $soname гарантирует минимальную бинарную совместимость...

Что до lua то никакой принципиальной разницы с перлом не вижу. Механизм lightuserdata это по сути тот же T_PTROBJ.

Date: 2007-01-31 10:09 pm (UTC)
From: [identity profile] avva.livejournal.com
Клей не бессмысленный, клей можно компилировать с -Wall -Werror, получая все статические проверки и т.п.

Это верно, не спорю. Но в случае, скажем, стандартизованных системных APIев это редко важно. В случае простой функции, которую просто хочешь вызвать, это часто совсем не нужно.

А если делать просто dlopen/dlsym и дальше вызывать функцию по адресу, то весь calling convention держится на честном слове.

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

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

Препроцессор недоступен. Многие вещи по части совместимости разных версий решаются на уровне препоцессора. Вот пример:


Вот для таких случаев пусть XS и живет, не спорю.

открывать ее просто так через симлинк /usr/lib/lib$name.so это....

Да я все делаю через DynaLoader, зачем же мне руками вызывать dl_open.

Date: 2007-01-31 10:26 pm (UTC)
From: [identity profile] ex-svpv.livejournal.com
Она и так держится на честном слове, просто это честное слово заголовка, который вы включаете.

Не совсем. Заголовок, как правило, включается в компилируемый код библиотеки на стадии сборки самой библиотеки. Этим отслеживается несовпадение прототипов. При рассогласовании прототипов библиотека просто не соберется. Т.е. при правильном подходе (включать заголовок перед реализацией, ср. -Wmissing-prototypes) прототипы из заголовка это значительно больше, чем просто честное слово.

Анатолий, Ваша идея мне понятна и мила. Это когда-то работало, может быть в начале семидесятых годов, на заре юникса. При случае взгляните в unix-v6/iolib/printf.c, это ещё до-stdio'шный код. Факт наверное в том, что сейчас такой подход не работает. Если не перестраховаться, то SEGV поджидает за ближайшим поворотом.

Как Вы загружаете библиотеку через DynaLoader без указания soname?

Date: 2007-01-31 11:08 pm (UTC)
From: [identity profile] avva.livejournal.com
С указанием soname, но не напрямую файла libsoname etc.

Date: 2007-01-31 11:29 pm (UTC)
From: [identity profile] ex-svpv.livejournal.com
Так не бывает. Или мы друг друга не понимаем. soname это грубо говоря то что показывает objdump -p /usr/lib/lib$name.so |grep SONAME. soname влияет на то как линкер будет компоновать с библиотекой. Ой ну ладно...

Date: 2007-01-31 11:39 pm (UTC)
From: [identity profile] avva.livejournal.com
Может, я торможу, не знаю. DynaLoader::dl_load_file принимает строку вида "libncurses.so.5", например. Которая и есть ее soname.

Date: 2007-01-31 10:12 pm (UTC)
From: [identity profile] cmm.livejournal.com
> Главное, что остается непонятным - зачем вообще нужно что-то компилировать.

термин, который вам с Галем, видимо, незнаком: "foreign function interface", или коротко ffi.

попробуй прогуглить "ffi Perl", и откроется тебе ненужность трудов твоих.

Date: 2007-01-31 10:20 pm (UTC)
From: [identity profile] avva.livejournal.com
Мне хорошо известен этот термин. Но, видимо, действительно, не поискал эти два слова вместе, хотя думал, что искал.

Спасибо!

Date: 2007-01-31 10:28 pm (UTC)
From: [identity profile] avva.livejournal.com
Правда, она не работает и даже не строится, и bitrot ее не пощадил, судя по всему. Но возможно проще будет ее исправить.

Date: 2007-02-01 06:35 am (UTC)
From: [identity profile] cmm.livejournal.com
твоя задача, собственно, в чём?

получить возможность с комфортом пользоваться динамическими библиотеками? (она у тебя есть, просто не в Перле).

улучшить Перл на годик-пару (до очередного bitrot-inducing изменения микроклимата)?

поиграть с FFI на уровне ABI?

Date: 2007-02-01 06:38 am (UTC)
From: [identity profile] avva.livejournal.com
у меня есть конкретный itch to scratch (хочу написать testing framework для нескольких разных библиотек на C, сравнивая их между собой, на перле это сделать удобнее и проще всего мне лично), плюс заинтересовала тема после того, как я понял, что сразу в нескольких языках, к-е я знаю, нет ничего удобного и известного.

Date: 2007-01-31 11:12 pm (UTC)
From: [identity profile] avnik.livejournal.com
Ну есть еще libffi(в комплекте gcc)/libffcall --- ими можно делать часть черной работы.
(Если еще к вашей бибилиотеке будет какой-то формат "опеисания" вызова -- чтобы его попытаться сделать стандартным -- то всем наступит счастье ;)

PS Основная кстати причина -- почему я интеграционным языком выбрал питон -- который я знаю слабо, а не перл который я знаю достаточно хорошо --- это то что к питону проще вязать С/С++ билиотеки.

Date: 2007-01-31 11:15 pm (UTC)
From: [identity profile] avnik.livejournal.com
http://search.cpan.org/~pmoore/FFI-1.00/Library.pm

И еще немножко его.
Вопрос в "стандартном описании reflections для C/C++"
(deleted comment)

Date: 2007-01-31 11:21 pm (UTC)
From: [identity profile] avva.livejournal.com
Да, cmm уже указал на модуль FFI, который я по какой-то странной причине пропустил, жаль, что у него мейнтейнер исчез и за 6 лет он прокоптился, но можно будет подкрутить, думаю, поглядим.

Date: 2007-01-31 11:35 pm (UTC)
From: [identity profile] avnik.livejournal.com
Я имел ввиду именно C часть -- там куча ассемблерных хаков -- и врядли есть смысл их повторять.
И/или libffcall там по моему немножко по другому но тоже самое ;)

Date: 2007-02-01 04:29 am (UTC)
From: [identity profile] quieres.livejournal.com
Почему вообще любой dll нельзя в чужой среде загружать и запускать его функции мне очень даже понятно: у него своя (свои) кучи. Поэтому если кто-то пишет нативный код для некоей исполняющей среды, он должен по крайней мере пользоваться её кучей, иначе всем кранты. Туда же интерфейсы с операционной системой, которые могут для процесса давать интересные сторонние эффекты наподобие запуска потоков, про которые исполняющая среда не знает. Вот и получается, что от нативного кода только хуже.

Date: 2007-02-01 06:20 am (UTC)
From: [identity profile] ban-dana.livejournal.com
Ну почему же сразу "кранты"? Если чужую память не освобождать - всё, по идее, нормально, нет?

Date: 2007-02-02 06:31 am (UTC)
netch: (Default)
From: [personal profile] netch
DLL - может быть. Для юникса и shared objects (.so) это в подавляющем большинстве случаев неверно, куча одна на всех.

Но уйти от зависимости от реализации (где-то куча общая, где-то частные) можно аккуратным построением API по принципу "освобождает тот, кто занял".

Date: 2007-02-02 07:03 am (UTC)
From: [identity profile] quieres.livejournal.com
"Освобождает тот, кто занял" - это иллюзия, ведь у всех подпрограмм одно адресное пространство. Куча ест минимум в 2 раза больше памяти, чем нужно на самом деле. Если говорить про windows, то там еще и двухэтапное выделение памяти (сначала зарезервировать непрерывный регион, потом по частям его начинать использовать) влияет, когда 30 пустых куч резервируют для себя 300 МБ адресного пространства.

Date: 2007-02-02 07:26 am (UTC)
netch: (Default)
From: [personal profile] netch
> "Освобождает тот, кто занял" - это иллюзия,

Нет.
Если Вы попытаетесь в случае DLL'ок у которых свои malloc'и вызвать free() в основной программе что получило malloc() в основной - в лучшем случае ничего не будет, в худшем программа завершится. free() должна делать та же DLL. Поэтому, увы, не иллюзия.

> ведь у всех подпрограмм одно адресное пространство.

Это не мешает вышеописанному эффекту.

> Если говорить про windows, то там еще и двухэтапное выделение памяти (сначала зарезервировать непрерывный регион, потом по частям его начинать использовать) влияет, когда 30 пустых куч резервируют для себя 300 МБ адресного пространства.

Да, но это уже другой вопрос.

Date: 2007-02-02 12:58 pm (UTC)
From: [identity profile] quieres.livejournal.com
Говоря про иллюзию, я имел в виду не то, что Вы подумали. Я взрослый мальчик и понимаю, что одна куча не может высвободить регион, который выделила другая.

Я имел в виду как раз фрагментацию куч, и как результат - рост объема занятой части адресного пространства пропорционально количеству реально используемой памяти не в первой степени. Частично эффект возникает потому, что это все-таки куча, но множитель количества куч тоже возвожится в не-первую степень. Таким образом, лучше пусть будет одна куча.

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

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

Date: 2007-02-01 07:56 am (UTC)
From: [identity profile] zverok-kha.livejournal.com
По опыту Руби: здесь есть и C-шный способ создания расширений (я не сталкивался с расширениями Python или Perl, но по отзывам тех кто сталкивался - в Руби этот интерфейс весьма прост и удобен) и библиотека Ruby/DL (), позволяющая всякие такие вещи:

require 'dl/import'
module LIBC
  extend DL::Importable
  dlload "libc.so"
  typealias ("size_t", "unsigned int")
  extern "void *qsort(size_t, size_t, void*)"
end


(это был чистый Руби, после этого описания можно вызывать LIBC::qsort)
Так вот, по опыту использования того и другого - программы с Ruby/DL достаточно неудобны в отладке - проще иметь свой написанный на C модуль, в котором можно наставить брекпойнтов и прочего. Но вот уже отлаженные библиотеки народ достаточно успешно переводит на Ruby-only versions (как правило, сопровождаемые предупреждениями "но эта версия намного медленнее" ;)

Date: 2007-02-01 11:15 am (UTC)
From: [identity profile] trurle.livejournal.com
Мне не очень понятно как можно устроить автоматический бесшовный интефейс между интерпретируемым/байткодным языком и функцией fstat(2).
Не говоря уже про opendir/readdir(3)

Date: 2007-02-01 02:40 pm (UTC)
From: [identity profile] cmm.livejournal.com
а каким образом релевантны интерпретируемость и байткодность?
(и, while you are at it, какая между ними связь?  и, раз уж на то пошло, интерпретируемость и байткодность являются свойствами реализации, а не языка).

Date: 2007-02-01 02:46 pm (UTC)
From: [identity profile] trurle.livejournal.com
По некоторому размышлению - никак не связаны, достаточно что бы представление структур и строчек во время исполнения радикально отличалось от сишных.
Можно, конечно, представить себе систему, автоматически генерирующую клей по сишным декларациям, но будет это как-то не очень весело, как мне кажется.

Date: 2007-02-01 02:56 pm (UTC)
From: [identity profile] cmm.livejournal.com
> Можно, конечно, представить себе систему, автоматически генерирующую клей по сишным декларациям, но будет это как-то не очень весело, как мне кажется.

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

Date: 2007-02-01 12:10 pm (UTC)
From: [identity profile] al-zatv.livejournal.com
А в .net'е этот вопрос решили на 5+. Использование нескольких языков доставляет массу удовольствия, и ни капли напрягов из-за бессмысленных клеев и "зависаний потому что pragma pack для структуры забыл поставить". Хотя я совсем не люблю microsoft, но начал пользоваться хорошим языком boo (boo.codehaus.org, как питон только лучше, по-моему). а так как он под .net, то пришлось пользоваться. Сперва с ненавистью, а потом понял всю кайфовость этого дела.

.Net это экстаз

Date: 2007-02-01 02:48 pm (UTC)
From: [identity profile] trurle.livejournal.com
Особенно церебрально-эротичен интерфейс между .Net'ом и бинарным C/C++ кодом, не завернутым в COM.

Re: .Net это экстаз

Date: 2007-02-01 03:08 pm (UTC)
From: [identity profile] al-zatv.livejournal.com
ну в таком случае, наверное, проще сделать прокладку на managed C++. но, не знаю, не пробовал. я говорю про взаимодействие CLI-шных языков.

Причём, всё могло бы быть почти столь же лёгким и в случае взаимодействия С++ с Perl, LUA, питонами и прочего, если бы разработчики С++ лет эдак десять назад сделали бы стандартный ABI для С++ (ну то есть стандарт на бинарное представление объектов), ну и может быть расширили бы стандарт объектных файлов. Это решило бы и кучу проблем в самом языке, и под этот стандарт прогнулись бы все прочие языки попроще, поперлее и попитонистее:) Тогда на то были все возможности: С++ был единственным серъёзным языком. Потом уже всякие джавы с дотнетами полезли, а теперь уже и возможность упущена.

Re: .Net это экстаз

Date: 2007-02-01 07:28 pm (UTC)
From: [identity profile] s1m.livejournal.com
В .НЕТе как раз все нормально сделано, достаточно extern + DllImport прибить к описанию метода и готово.

Re: .Net это экстаз

Date: 2007-02-01 07:37 pm (UTC)
From: [identity profile] s1m.livejournal.com
Может у вас и "щазз", а мы через C-ный OCI в Оракл ходим за нужными фишками и все работает как часы :P

Date: 2007-02-01 06:54 pm (UTC)
From: [identity profile] madfire.livejournal.com
Странно, что ты не нашел такой библиотеки в руби.
Предвзятость? или питонопоклонники тебе платят? :)

January 2026

S M T W T F S
    1 2 3
4 5 6 7 8 910
11121314151617
18192021222324
25262728293031

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Jan. 12th, 2026 02:43 am
Powered by Dreamwidth Studios