- PVSM.RU - https://www.pvsm.ru -
В статье описан способ построения моста между неуправляемым и управляемым кодом на примере математического пакета Mathcad. На картинке показан пример, как бурундук Тот собирается обрабатывать своё изображение средствами математического пакета. Для этого он «использовал» пользовательскую функцию, написанную на VB.Net, в которой реализована возможность подключения к веб-камере и создания снимка. Результат работы функции сразу доступен в рабочем документе.
Для нетерпеливых, кто хочет понять всё сразу, пробежав по-диагонали код, указываю хранилище: NetEFI [1]. Там же можно найти тестовые пользовательские библиотеки на трёх языках: c#, vb.net и c++/cli (VS2012, .Net 2.0, x86-32). Пока доступна только 32-разрядная реализация.
В математической программе Mathcad существует возможность подключения сторонних библиотек. Называется этот интерфейс User EFI и был разработан больше 10 лет тому назад. С тех пор он не менялся вообще, хотя сам Mathcad изменился до неузнаваемости. Было время, когда этот интерфейс выкинули из пакета, но старые пользователи затребовали его обратно и в новых версиях Mathcad Prime этот раритетный интерфейс снова живее всех живых.
Существует довольно доходчивое руководство по созданию пользовательских библиотек, я привёл его в конце статьи. Если вкратце, то процесс выглядит примерно так. Мы создаём обычную dll, где в точке входа, т.е. при её загрузке, регистрируем наши функции. При этом, в описателе функции указываем её адрес для последующего вызова из Mathcad напрямую. Кроме этого, ещё можно зарегистрировать одну таблицу с сообщениями об ошибках. Результат, возвращаемый функцией пользователя в случае ошибки, может использоваться для выбора сообщений из этой таблицы. Вот в общем и вся кухня.
Описатель функции выглядит так:
typedef LRESULT (* LPCFUNCTION ) ( void * const, const void * const, ... );
// The FUNCTIONINFO structure contains the information that Mathcad uses to register a
// user function. Refer below for each member and its description.
typedef struct tagFUNCTIONINFO {
// Points to a NULL-terminated string that specifies the name of the user
// function.
char * lpstrName;
// Points to a NULL-terminated string that specifies the parameters of the
// user function.
char * lpstrParameters;
// Points to a NULL-terminated string that specifies the function description.
char * lpstrDescription;
// Pointer to the code that executes the user function.
LPCFUNCTION lpfnMyCFunction;
// Specifies the type of value returned by the function. The values are
// COMPLEX_ARRAY or COMPLEX_SCALAR.
long unsigned int returnType;
// Specifies the number of arguments expected by the function. Must be
// between 1 and MAX_ARGS.
unsigned int nArgs;
// Specifies an array of long unsigned integers containing input parameter
// types.
long unsigned int argType[ MAX_ARGS ];
} FUNCTIONINFO;
Проблема в том, что сегодня можно было бы гораздо удобнее писать свои функции, если бы мы делали это на .net языках. Но прямой путь для этого лежит через использование C++/CLI. Вариант «обёртывания» каждой пользовательской функции через переходник на C++/CLI или маршалинг структур, думаю, можно сразу отметать как непрактичный и требующий нетривиальных познаний от пользователя математической программы. Я хочу предложить универсальную «обёртку», которую назвал .Net User EFI.
Возникает вопрос, а как создать универсальную функцию, которую можно было бы регистрировать вместо всех функций всех подключаемых сборок, но при этом в точке входа она обладала бы всей необходимой информацией для вызова конкретной функции из конкретной сборки. Библиотека посредник, в которой такая функция находится, должна автоматически работать с любым количеством подключаемых сборок и функций в них.
Для реализации такой универсальности есть одна существенная проблема. Mathcad требует указывать адрес вызываемой функции, сам же прототип объявлен как имеющий переменное количество параметров. Получается, что в точке входа универсальной функции стек с параметрами будет иметь разный размер и передать эту информацию при вызове функции стандартными средствами нет никакой возможности, т.к. она определяется самим скомпилированным кодом. В структуре выше только сам адрес выступает в качестве параметра, по которому мы могли бы отличить вызов одной функции от другой.
И тут наша мысль должна прийти к одному известному решению, которое называется инъекцией кода. На хабре не раз об этом писали, но вот практических полезных примеров использования такой техники можно найти не так много. В каком-то смысле мы тоже будем перехватывать вызовы функций из dll, но в нашем случае всё будет выглядеть немного специфичней, но гораздо проще.
Итак, что же мы будем инъецировать, внедрять, куда и зачем. Ещё раз проясним ситуацию. Мы хотим написать универсальную функцию, которая будет единообразно обрабатывать все вызовы и распределять их в зависимости от типа вызываемой функции. Mathcad не должен ничего «заподозрить», а у нас должна откуда-то взяться дополнительная информация в точке входа универсальной функции о параметрах вызова.
Решение будет в динамическом формировании кода по адресу, который мы регистрируем в Mathcad. Мы зарезервируем в памяти много места под динамический код. Этот код будет осуществлять вспомогательную работу по передаче параметров универсальной функции. Наперёд скажу, что нам достаточно два параметра, это номер сборки в массиве загруженных сборок и номер функции из сборки. Существует два пути передачи параметров: глобальные переменные и стек. Я выбрал первый вариант, т.к. нарушить баланс стека (в котором находятся параметры) легко, а вот восстановить его в нашем случае, я думаю, будет сложно.
Забыл упомянуть, что типов параметров у функции пользователя всего три и все они передаются по указателю: MCSTRING, COMPLEXSCALAR и COMPLEXARRAY. Максимальное их число также ограничено — 10 штук. Это упрощает реализацию разбора параметров в универсальной функции.
Теперь мы морально готовы, чтобы разобрать конкретную последовательность событий, которая должна происходить на этапе внедрения и после него.
Шаг 1. Пользователь создаёт .net класс, реализующей интерфейс IFunction, который содержит необходимую информацию о функции. Компилирует его в сборку и копирует в папку userefi. Также в этой папке должна находиться сборка-посредник, будем называть её netefi.
Шаг 2. При загрузке Mathcad сборка-посредник netefi воспринимается как пользовательская библиотека. Она осуществляет поиск всех .net сборок в текущей папке и перебора функций в них на предмет реализации интерфейса IFunction.
Шаг 3. netefi сохраняет информацию о сборках и функциях в них во внутренних массивах, при этом, чтобы определить функцию, нужно два числа: индекс сборки и индекс функции в ней.
Шаг 4. netefi перебирает все функции и регистрирует их в Mathcad стандартным образом, но в поле адреса структуры FUNCTIONINFO мы записываем ссылку на динамический код, вид которого определяется двумя индексами из предыдущего шага.
Вот так выглядит конкретная реализация метода внедрения:
static int assemblyId = -1;
static int functionId = -1;
static PBYTE pCode = NULL;
#pragma unmanaged
LRESULT CallbackFunction( void * out, ... ) {
return ::UserFunction( & out );
}
#pragma managed
// TODO: 64-bit.
void Manager::InjectCode( PBYTE & p, int k, int n ) {
// Пересылка константы (номера сборки) в глобальную переменную.
* p++ = 0xB8; // mov eax, imm32
p[0] = k;
p += sizeof( int );
* p++ = 0xA3; // mov [assemblyId], eax
( int * & ) p[0] = & assemblyId;
p += sizeof( int * );
// Пересылка константы (номера функции) в глобальную переменную.
* p++ = 0xB8; // mov eax, imm32
p[0] = n;
p += sizeof( int );
* p++ = 0xA3; // mov [functionId], eax
( int * & ) p[0] = & functionId;
p += sizeof( int * );
// jmp to CallbackFunction.
* p++ = 0xE9;
( UINT & ) p[0] = ( PBYTE ) ::CallbackFunction - 4 - p;
p += sizeof( PBYTE );
}
Метод InjectCode() вызывается в цикле при регистрации функций в Mathcad. Глобальные переменные assemblyId и functionId используются для определения типа функции во время её вызова. Работает это так. Mathcad для каждой функции получает ссылку на такой вот динамический код. При этом в assemblyId записывается индекс сборки, известный на момент загрузки (параметр k), в functionId записывается индекс функции — параметр n. Далее идёт безусловный переход на CallbackFunction(), в которой вызывается наша универсальная функция. Это сделано для того, чтобы можно было в UserFunction() вызывать управляемый код. Директивы unmanaged / managed не дадут этого сделать в CallbackFunction().
Заметьте, что параметром универсальной функции является ссылка на стек CallbackFunction(), т.е. на массив параметров (возвращаемое значение находится там же). Динамический код не портит нам стек, поэтому после завершения CallbackFunction() управление вернётся к Mathcad. Вот и вся магия.
Шаг 5. После того, как регистрация завершена, вы можете вызвать пользовательскую функцию в документе Mathcad. Универсальная функция UserFunction() теперь может восстановить тип функции пользователя по глобальным параметрам assemblyId и functionId и разобрать стек, зная количество и тип параметров.
Шаг 6. Каждый неуправляемый тип параметра функции заменяется на аналог: MCSTRING на String, COMPLEXSCALAR на TComplex (я не стал использовать Complex из .Net 4.0, чтобы не было конфликта) и COMPLEXARRAY на TComplex[,].
Шаг 7. Вызывается реализация метода IFunction.NumericEvaluation для функции. Возвращаемый результат проходит обратную последовательность преобразований и отдаётся в Mathcad.
Думаю, что этот конкретный способ внедрения я объяснил более менее понятно. Что касается непосредственно самих исходников проекта, то стоит вкратце упомянуть окружение и некоторые детали. В качестве среды разработки используется Visual Studio 2012, язык C++/CLI, .Net Framework 2.0 (выставлен соответствующий режим в свойствах проектов). Поскольку динамический код, вообще говоря, зависит от разрядности и я ещё не знаю точно как привести его к 64-битному представлению, то все проекты настроены на компиляцию для 32-битных машин. Хотя мне говорили [2], что изменений будет не много.
Использование глобальных переменных нехорошо, но работа в Mathcad не предполагает одновременный вызов нескольких функций. Там всё делается по-порядку, друг за другом.
В сборке-посреднике реализованы ещё некоторые идеи, которые позволяют полно использовать старый интерфейс в новом окружении. Это касается обработки ошибок и об этом нужно писать отдельно. Весь основной код сосредоточен в одном единственном классе Manager (netefi.cpp). Разбирая тестовые примеры, можно понять как работать с интерфейсом IFunction. Все тестовые примеры на разных языках делают одно и то же, и называются почти одинаково.
Примеры тестируются в Mathcad 15 и Mathcad Prime 3.0. Поскольку сам интерфейс User EFI не менялся больше 10 лет (и вряд ли уже изменится), то можно использовать описанный метод и в других версиях Mathcad, начиная, наверное, с 11 версии. В Mathcad Prime 3.0 пользовательским функциям дали новое название — Custom Functions, хотя начинка та же.
Как было указано выше, вы можете найти их тут [1]. Но статья была бы не полной, если не показать конкретный вид .net пользовательских функций для Mathcad.
Посмотрим как будет выглядеть функция «эхо» для одного строкового параметра.
using System;
using NetEFI;
public class csecho: IFunction {
public FunctionInfo Info {
get {
return new FunctionInfo( "csecho", "s", "return string",
typeof( String ), new[] { typeof( String ) } );
}
}
public FunctionInfo GetFunctionInfo( string lang ) { return Info; }
public bool NumericEvaluation( object[] args, out object result ) {
result = args[0];
return true;
}
}
Imports NetEFI
Public Class vbecho
Implements IFunction
Public ReadOnly Property Info() As FunctionInfo _
Implements IFunction.Info
Get
Return New FunctionInfo("vbecho", "s", "return string", _
GetType([String]), New Type() {GetType([String])})
End Get
End Property
Public Function GetFunctionInfo(lang As String) As FunctionInfo _
Implements IFunction.GetFunctionInfo
Return Info
End Function
Public Function NumericEvaluation(args As Object(), ByRef result As Object) As Boolean _
Implements IFunction.NumericEvaluation
result = args(0)
Return True
End Function
End Class
#pragma once
using namespace System;
using namespace System::Text;
using namespace NetEFI;
public ref class cppecho: public IFunction {
public:
virtual property FunctionInfo^ Info {
FunctionInfo^ get() {
return gcnew FunctionInfo( "cppecho", "s", "return string",
String::typeid, gcnew array<Type^> { String::typeid } );
}
}
virtual FunctionInfo^ GetFunctionInfo(String^ lang) { return Info; }
virtual bool NumericEvaluation( array< Object^ > ^ args, [Out] Object ^ % result ) {
result = args[0];
return true;
}
};
Хотя основной функционал уже практически готов, есть некоторые недоделки. К примеру, желательно, чтобы работа универсальной функции выполнялась в отдельном потоке. Эта одна из первых вещей, которые надо реализовать. Прерывание работы путём вызова isUserInterrupted никак не отражена в новом интерфейсе. Вся надежда пока на то, что сам Mathcad может прервать работу функции. Над этим буду думать и это перекликается с работой в потоке.
Текущий проект пока работает только на 32-битных системах. Для добавления 64-битных конфигураций нужно протестировать работу динамического кода на 64-битных системах. Пока нет такой возможности.
Работа с COM внутри пользовательской функции сейчас тоже, видимо, невозможна. Столкнулся я с этим, когда реализовывал функцию для создания снимка с веб-камеры. Один из стандартных вариантов предполагал использовать интерфейс к Clipboard, так вот он не заработал, сообщив о том, что поток должен быть с атрибутом STAThreadAttribute. Решил проблему через Graphics.CopyFromScreen. Тоже нужно разбираться.
Загрузка недостающих сборок также пока сделана не достаточно надёжно, т.к. используется Assembly::LoadFile(). Если же использовать Assembly::LoadFrom(), то Mathcad зависает в этом месте. Есть ещё проблема с отладкой смешанного кода. Почему-то она у меня не заработала как надо. Я практически в уме отлаживал код, спасали только логи.
Может быть кто-то делал что-то подобное и мог бы подсказать хорошие идеи, чтобы упростить мой код. Выслушаю все практические варианты. Было бы вообще замечательно, если бы кто заставил мой проект работать под отладчиком студии в смешанном режиме. Пока работают только точки останова в неуправляемом коде. В тестовых примерах бродить по коду можно, конечно.
1. Исходники и тестовые примеры на github [1].
2. Creating a User DLL [3] (pdf).
3. .Net User EFI interface [4] (ветка на основном форуме PTC).
4. Исходники и сборки примера с веб-камерой [5] (в той же ветке ниже).
5. Mathcad EFI plugin [6] (другой мой проект, который выполняет обратную функцию — вызывает неуправляемый код из управляемого).
Автор: ViacheslavMezentsev
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-3/63648
Ссылки в тексте:
[1] NetEFI: https://github.com/ViacheslavMezentsev/NetEFI
[2] мне говорили: http://stackoverflow.com/questions/24466513/calculating-jmp-instructions-address-x86-64
[3] Creating a User DLL: http://en.smath.info/forum/resource.ashx?a=1475
[4] .Net User EFI interface: http://communities.ptc.com/message/247400#247400
[5] Исходники и сборки примера с веб-камерой: http://communities.ptc.com/message/247740#247740
[6] Mathcad EFI plugin: http://en.smath.info/forum/yaf_postst755_Mathcad-EFI-plugin.aspx
[7] Источник: http://habrahabr.ru/post/227455/
Нажмите здесь для печати.