Хочу поделиться историей из профессиональной деятельности, которую можно заслуженно поместить в блог с именем crazydev :) Это рассказ о необычных решениях (тех, что я попытался описать в двух словах в заголовке), к которым меня вынудили прийти еще более необычные ограничения и требования.
И вот как-то так, через хитро закрученную ***у, оно и работает ©
Постановка проблемы
Несколько лет назад работали над системой, предназначенной для <a rel="nofollow" href="http://ru.wikipedia.org/wiki/%D0%9C%D0%BE%D0%B1%D0%B8%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F_%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B0_%D0%92%D0%BE%D0%BE%D1%80%D1%83%D0%B6%D1%91%D0%BD%D0%BD%D1%8B%D1%85_%D0%A1%D0%B8%D0%BB">платформы наших доблестных ВС. Существенная часть работы протекала в портировании (с windows) уже имеющегося софта. И тут оказывается: важный компонент системы (назовем его библиотекой сепуления), предоставленный партнером, попросту не имеет версии под linux, и его разработчик вообщем-то не спешит с портом, т.к. нет таких договоренностей. Ругаться со своим начальством, допустившим этот промах при планированний работ бессмысленно — если в итоге сепулькарии будут стоять, то это незакрытый ТЗ, и виноваты в первую очередь будем мы, как исполнители.
Поиск решений
Вначале здесь была стена текста о перебираемых вариантах решений, которые включали в себя и портирование исходников своими силами, и использований виртуальной машины с Реактивной Осью (дабы не задействовать проприетарный софт вероятного противника), и включение в локальнюю сеть системника в форм-факторе Mini-ITX с той же ReactOS на борту. И напряженный поиск более-менее стабильной версии wine (системные библиотеки довольно старые, а обновлять нельзя — грозит потерей сертификации ОС).
На варианте эмуляции с помощью Wine (который, как вы знаете, вовсе не эмулятор) мы и остановились. Оставалось продумать, как сепулькарии запускаемые внутри процесса серверного софта будут получать доступ к алгоритмам сепуления, вынуждено находящимся под юрисдикцией wine-процесса. Тут мне приходит в голову идея — организовать сетевой транслятор обращения к библиотекам.
Транслятор
В общем виде это выглядит так:
(кстати, похоже, получается одна из вариаций модели «Программа как услуга» (Soft as a service), но это уже тема другого рассказа)
Что мне кажется интересным в такой схеме, так это то, что клиенты могут работать как в разных инстанциях выбранной библиотеки, так и в одной и той же.
Для реализации есть два пути:
1) Обучить транслятор работе с набором необходимых сейчас библиотек и и потом в случае пополнения этого набора каждый раз дописывать его код (или подключать через адаптеры, не суть важно), обеспечивая сопряжение с каждой новой библиотекой.
2) Обеспечить универсальность транслятора, сделав его по-настоящему просто транслятором запросов, перекладывая функцию формирования нужных запросов к новым библиотекам на клиентскую часть.
Очевидно, если бы я пошел по первому пути, писать в crazydev мне было бы нечего :)
Универсальность повсюду
Простой пример подключения библиотеки и вызова из неё функции.
typedef double (*myfunc_type)(long, long); ... void *mylib; myfunc_type myfunc; double res; ... mylib = dlopen("mylib.dll", RTLD_LAZY); myfunc = dlsym(mylib, "my_func_name"); res = (*myfunc)(2, 4);
Как мы видим, для того чтобы обратиться к какой либо функции, мы должны предварительно описать её тип:
typedef double (*myfunc_type)(long, long);
А что делать когда типы функций, к которым мы будем обращаться, неизвестны на этапе разработки?
Всё правильно, нам приходит на помощь старый добрый ассемблер. Что, по сути, вызов функции? Помещение аргументов в стек, передача управления адресу, дальше получение результата, и, в зависимости от соглашения вызова, очистка стека.
Вот кусок кода (на Delphi), как раз выполнявший в моем трансляторе подобные операции (Внес дополнительные комментарии чтобы исключить непонятные моменты):
//Метод осуществляет непосредственный вызов функции, //которую обслуживает объект класса TDLL_Function //Возвращает байтовый массив, содержащий результат функции (если он предусмотрен) function TDLL_Function.Execute(): TByteAr; var i: Integer; //Итератор для цикла len : Integer; //Длина байтового массива со значениями параметров B1 : Byte; //Буфер для однобайтового параметра B2 : Word; //Буфер для двубайтового параметра B4 : Cardinal; //Буфер для 4-байтового параметра B8 : Double; // Буфер для 8-байтового параметра StackPos : Integer; //Переменная для хранения позиции стека begin //ParamBytes это поле класса, байтовый массив содержащий все значения параметров, //которые должны быть переданы в функцию len:=Length(ParamBytes); asm //Запоминаем начальное значение стека (из регистра esp) mov StackPos, esp end; //ParamType это поле класса, массив значений следующего перечисляемого типа: //TParamType = (ptOne=1, ptTwo=2, ptFour=4, ptEight=8, ptVoid=0, ptPointer=-1); //Содержит типы параметров, которые должны быть переданы в функцию //Перебираем массив с конца, т.к. помещать параметры в стек нужно в обратном порядке for i := Length(ParamType)-1 downto 0 do begin case ParamType[i] of //В зависимости от типа текущего элемента ptOne:begin dec(len,1); //Извлекаем из ParamBytes нужное число байт (с конца), кладем в буфер Move(ParamBytes[len],B1,1); asm //Буфер в регистр, а оттуда в стек MOVSX EAX,B1 PUSH EAX end; end; ptTwo:begin //Тоже самое для двух байт dec(len,2); Move(ParamBytes[len],B2,2); asm MOVSX EAX,B2 PUSH EAX end; end; ptFour:begin //Тоже самое для четырех байт dec(len,4); Move(ParamBytes[len],B4,4); asm MOV EAX,B4 PUSH EAX end; end; //Для указателей пока тоже самое что для четырех байт, но ведь поддержка //64-битных систем (8 байт на указатель) есть в перспективе ptPointer:begin dec(len,4); Move(ParamBytes[len],B4,4); asm MOV EAX,B4 PUSH EAX end; end; ptEight:begin dec(len,8); Move(ParamBytes[len],B8,8); asm //Немного другой вид инструкций для помещения восьми байт PUSH DWORD PTR [B8]+$04 PUSH DWORD PTR B8 end; end; ptVoid: begin end; end; end; //Теперь B4 используется как буфер для чтения результата case fCallingConv of //Дальше в зависимости от типа соглашения вызова ccStdcall: begin TStdCall(Proc)(); //Proc - указатель на функцию библиотеки asm //Считали результат из регистра MOV B4,EAX end; end; ccCdecl:begin TCdeclCall(Proc)(); //В случае с Cdecl вызовом мы еще и возвращаем стек в начальное положение asm MOV B4,EAX mov esp, StackPos; end; end; end; //Если результат не предусмотрен, то мы считали мусор case ResultType of //В зависимости от типа результата ptOne:begin //Помещаем в массив Result нужное нам число байт из B4 SetLength(Result,1); Move(Byte(B4),Result[0],1); end; ptTwo:begin SetLength(Result,2); Move(Word(B4),Result[0],2); end; ptFour:begin SetLength(Result,4); Move(B4,Result[0],4); end; ptPointer:begin SetLength(Result,4); Move(B4,Result[0],4); end; ptEight:begin //Отдельный случай для восьми байт, используем B8 в роли буфера asm FSTP B8 end; SetLength(Result,8); Move(B8,Result[0],8); end; ptVoid:begin SetLength(Result,0); end; end; end;
Прошу не бить ногами за код, он определенно требует оптимизации.
Про особенности реализации:
1) Доступными сделал только stdcall и cdecl соглашения
2) Нет никаких гарантий в том, что ассемблерные вставки будут так же работать на архитектурах, отличных от тех, под которые это делалось.
1) Нет поддержки 64-битного кода, хотя в целом, кое-какие пути для обеспечения я закладывал
2) Если в функцию передается указатель, например, на массив, то клиент должен был переправить массив целиком, чтобы транслятор развернул его у себя и отправил в функцию указатель на него. Если этот массив нужно было вернуть обратно, то подобным образом организовывалось и его возвращение.
Вообще протокол для общения клиента и транслятора получился достаточно сложным и запутанным, и не знаю, имеет ли смысл его описывать. Скажу только, что он был бинарный :)
Общая последовательность действий для вызова выглядела так:
1) Клиент подключается к транслятору
2) Клиент отправляет имя библиотеки, к которой хочет подключиться
3) Клиент отправляет описание функции библиотеки, которую хочет вызвать
4) Клиент пакует и отправляет параметры для вызова функции библиотеки
5) Транслятор разворачивает параметры в соответствии с полученным описанием функции
6) Транслятор вызывает вышеупомянутый Execute()
7) Транслятор пакует нужные итоги работы (согласно описанию функцию) и отправляет их клиенту.
Вот такой crazydev :) В защиту своей поделки скажу, что в таком виде она безотказно проработала год, и, воспользовавшись этой универсальностью, удалось сократить время на портирование и некоторых других библиотек. А через год, во время планового обновления, уже поспела портированная версия сепулических библиотек, и всё закончилось хорошо.
Автор: augur