Хочу поделиться историей из профессиональной деятельности, которую можно заслуженно поместить в блог с именем crazydev :) Это рассказ о необычных решениях (тех, что я попытался описать в двух словах в заголовке), к которым меня вынудили прийти еще более необычные ограничения и требования.
И вот как-то так, через хитро закрученную ***у, оно и работает ©
Несколько лет назад работали над системой, предназначенной для платформы наших доблестных ВС. Существенная часть работы протекала в портировании (с windows) уже имеющегося софта. И тут оказывается: важный компонент системы (назовем его библиотекой сепуления), предоставленный партнером, попросту не имеет версии под linux, и его разработчик в общем-то не спешит с портом, т.к. нет таких договоренностей. Ругаться со своим начальством, допустившим этот промах при планированний работ бессмысленно — если в итоге сепулькарии будут стоять, то это незакрытый ТЗ, и виноваты в первую очередь будем мы, как исполнители.
Вначале здесь была стена текста о перебираемых вариантах решений, которые включали в себя и портирование исходников своими силами, и использований виртуальной машины с Реактивной Осью (дабы не задействовать проприетарный софт вероятного противника), и включение в локальную сеть системника в форм-факторе Mini-ITX с той же ReactOS на борту. И напряженный поиск более-менее стабильной версии wine (системные библиотеки довольно старые, а обновлять нельзя — грозит потерей сертификации ОС).
На варианте эмуляции с помощью Wine (который, как вы знаете, вовсе не эмулятор) мы и остановились. Оставалось продумать, как сепулькарии запускаемые внутри процесса серверного софта будут получать доступ к алгоритмам сепуления, вынуждено находящимся под юрисдикцией wine-процесса. Тут мне приходит в голову идея — организовать сетевой транслятор обращения к библиотекам.
В общем виде это выглядит так:
(кстати, похоже, получается одна из вариаций модели «Программа как услуга» (Soft as a service), но это уже тема другого рассказа)
Что мне кажется интересным в такой схеме, так это то, что клиенты могут работать как в разных инстанциях выбранной библиотеки, так и в одной и той же.
Для реализации есть два пути:
1) Обучить транслятор работе с набором необходимых сейчас библиотек и и потом в случае пополнения этого набора каждый раз дописывать его код (или подключать через адаптеры, не суть важно), обеспечивая сопряжение с каждой новой библиотекой.
2) Обеспечить универсальность транслятора, сделав его по-настоящему просто транслятором запросов, перекладывая функцию формирования нужных запросов к новым библиотекам на клиентскую часть.
Очевидно, если бы я пошел по первому пути, писать в crazydev мне было бы нечего :)
Простой пример подключения библиотеки и вызова из неё функции.
Как мы видим, для того чтобы обратиться к какой либо функции, мы должны предварительно описать её тип:
А что делать когда типы функций, к которым мы будем обращаться, неизвестны на этапе разработки?
Всё правильно, нам приходит на помощь старый добрый ассемблер. Что, по сути, вызов функции? Помещение аргументов в стек, передача управления адресу, дальше получение результата, и, в зависимости от соглашения вызова, очистка стека.
Вот кусок кода (на Delphi), как раз выполнявший в моем трансляторе подобные операции (Внес дополнительные комментарии чтобы исключить непонятные моменты):
Прошу не бить ногами за код, он определенно требует оптимизации.
Про особенности реализации:
1) Доступными сделал только stdcall и cdecl соглашения
2) Нет никаких гарантий в том, что ассемблерные вставки будут так же работать на архитектурах, отличных от тех, под которые это делалось.
1) Нет поддержки 64-битного кода, хотя в целом, кое-какие пути для обеспечения я закладывал
2) Если в функцию передается указатель, например, на массив, то клиент должен был переправить массив целиком, чтобы транслятор развернул его у себя и отправил в функцию указатель на него. Если этот массив нужно было вернуть обратно, то подобным образом организовывалось и его возвращение.
Вообще протокол для общения клиента и транслятора получился достаточно сложным и запутанным, и не знаю, имеет ли смысл его описывать. Скажу только, что он был бинарный :)
Общая последовательность действий для вызова выглядела так:
1) Клиент подключается к транслятору
2) Клиент отправляет имя библиотеки, к которой хочет подключиться
3) Клиент отправляет описание функции библиотеки, которую хочет вызвать
4) Клиент пакует и отправляет параметры для вызова функции библиотеки
5) Транслятор разворачивает параметры в соответствии с полученным описанием функции
6) Транслятор вызывает вышеупомянутый Execute()
7) Транслятор пакует нужные итоги работы (согласно описанию функцию) и отправляет их клиенту.
Вот такой crazydev :) В защиту своей поделки скажу, что в таком виде она безотказно проработала год, и, воспользовавшись этой универсальностью, удалось сократить время на портирование и некоторых других библиотек. А через год, во время планового обновления, уже поспела портированная версия сепулических библиотек, и всё закончилось хорошо.
UPD. Добавил тег «костыли», чтобы не было непоняток с самоидентификацией описанных решений.
И вот как-то так, через хитро закрученную ***у, оно и работает ©
Постановка проблемы
Несколько лет назад работали над системой, предназначенной для платформы наших доблестных ВС. Существенная часть работы протекала в портировании (с 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 :) В защиту своей поделки скажу, что в таком виде она безотказно проработала год, и, воспользовавшись этой универсальностью, удалось сократить время на портирование и некоторых других библиотек. А через год, во время планового обновления, уже поспела портированная версия сепулических библиотек, и всё закончилось хорошо.
UPD. Добавил тег «костыли», чтобы не было непоняток с самоидентификацией описанных решений.