Внедрение кода с пользой



    В статье описан способ построения моста между неуправляемым и управляемым кодом на примере математического пакета Mathcad. На картинке показан пример, как бурундук Тот собирается обрабатывать своё изображение средствами математического пакета. Для этого он «использовал» пользовательскую функцию, написанную на VB.Net, в которой реализована возможность подключения к веб-камере и создания снимка. Результат работы функции сразу доступен в рабочем документе.

    Исходники


    Для нетерпеливых, кто хочет понять всё сразу, пробежав по-диагонали код, указываю хранилище: NetEFI. Там же можно найти тестовые пользовательские библиотеки на трёх языках: c#, vb.net и c++/cli (VS2012, .Net 2.0, x86-32). Пока доступна только 32-разрядная реализация.

    Предыстория


    В математической программе Mathcad существует возможность подключения сторонних библиотек. Называется этот интерфейс User EFI и был разработан больше 10 лет тому назад. С тех пор он не менялся вообще, хотя сам Mathcad изменился до неузнаваемости. Было время, когда этот интерфейс выкинули из пакета, но старые пользователи затребовали его обратно и в новых версиях Mathcad Prime этот раритетный интерфейс снова живее всех живых.

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

    Описатель функции выглядит так:

    Структура FUNCTIONINFO
    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-битных машин. Хотя мне говорили, что изменений будет не много.

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

    В сборке-посреднике реализованы ещё некоторые идеи, которые позволяют полно использовать старый интерфейс в новом окружении. Это касается обработки ошибок и об этом нужно писать отдельно. Весь основной код сосредоточен в одном единственном классе Manager (netefi.cpp). Разбирая тестовые примеры, можно понять как работать с интерфейсом IFunction. Все тестовые примеры на разных языках делают одно и то же, и называются почти одинаково.

    Примеры тестируются в Mathcad 15 и Mathcad Prime 3.0. Поскольку сам интерфейс User EFI не менялся больше 10 лет (и вряд ли уже изменится), то можно использовать описанный метод и в других версиях Mathcad, начиная, наверное, с 11 версии. В Mathcad Prime 3.0 пользовательским функциям дали новое название — Custom Functions, хотя начинка та же.

    Тестовые примеры


    Как было указано выше, вы можете найти их тут. Но статья была бы не полной, если не показать конкретный вид .net пользовательских функций для Mathcad.

    Посмотрим как будет выглядеть функция «эхо» для одного строкового параметра.

    C# вариант
    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;
        }
    
    }

    VB.Net вариант
    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

    С++/CLI вариант
    #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 зависает в этом месте. Есть ещё проблема с отладкой смешанного кода. Почему-то она у меня не заработала как надо. Я практически в уме отлаживал код, спасали только логи.

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

    Ссылки


    0. How to generate and run native code dynamically?
    1. Исходники и тестовые примеры на github.
    2. Creating a User DLL (pdf).
    3. .Net User EFI interface (ветка на основном форуме PTC).
    4. Исходники и сборки примера с веб-камерой (в той же ветке ниже).
    5. Mathcad EFI plugin (другой мой проект, который выполняет обратную функцию — вызывает неуправляемый код из управляемого).
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 14

      0
      Если не секрет, зачем?.. Например, в нашем университете обработку изображений целиком и полностью пишут на C#. Если нужна какая-то универсальная математика, графика, веб-камера — можно воспользоваться языком Python. И это решение будет лишено проблем склеивания с маткадом. Не говорю уже про бесплатность…
        +3
        Старый добрый Mathcad. Я привык к нему, как инструменту.

        Я когда-то тоже занимался обработкой изображений. Хотя я мог бы писать что-то на c#, но прототипы идей я всегда делал в Mathcad. Язык программирования отвлекает своими деталями от реализации математического алгоритма.

        У нас в универе было так. Собираются математики, которые ничего не понимают в программировании, и приглашают программистов, которые не так сильно разбираются в математике. Математики пишут на доске заумные формулы, а программисты чешут репу, пытаясь представить это дело в удобном для себя виде. Написали, значит, формулы и говорят, что вот тут они не уверены в результате и формула выведена вчера вечером, нельзя ли по-быстрому попробовать? Ну..., скажут программисты, сначала нужно написать программу, интерфейс для неё, формулы заумные перевести.

        А я делал всё в Mathcad для прототипирования и не заморачивался. Интерфейса не надо, численной математики не надо, а если, не дай боже, нужно жесткий дифур порешать? Тут сначала нужно найти библиотеку на c#, которая бы это осилила, потом понять как с ней работать… в общем, я думаю, мысль понятна.
          0
          Та же история. Занимаемся обработкой изображений на С++, все прототипы всегда пишем в Матлабе. Ибо скорость разработки выше раз в 100. Без преувеличений.
            0
            Да, кстати, я использовал оба пакета. Просто в Mathcad удобно на листе работать. Разбросал код по документу, какой не надо отключил, какой надо включил. Есть некоторая свобода, но matlab сильней в плане поддержки. Ну и язык программирования тоже не сравнить, ещё и отладчик встроенный.

            Я даже как-то из-за такого раздвоения писал пользовательские библиотеки, которые одновременно работали и в mathcad, и в matlab, используя отложенную загрузку dll. Когда программа отлажена в matlab'е, я её переписывал на C, чтобы быстрее работала и подключал как mex-функцию. После тестирования в matlabe я допиливал библиотеку, чтобы она могла одновременно работать и в mathcad. Теперь можно было делать красивые отчёты на базе документов mathcad.
        0
        Я бы подобную задачу попытался решить полностью в управляемом коде. Можно нагенерировать проксей c MarshalAsAttribute в параметрах методов через Reflection.Emit и использовать ICustomMarshaller для передачи параметров в неуправляемый код. Разве что не уверен, что ModuleInitializer будет корректной заменой DllMain.
          0
          К сожалению, у меня нет таких познаний в этой области и, откровенно говоря, этот маршалинг всегда действовал мне на нервы. c++/cli тоже действовал (надо думать одновременно на двух языках), но я с ним смирился.

          А как же с динамическим кодом быть? Фишка основная в том, что я «подменяю» неупраляемую callback-функцию, которая имеет переменное количество параметров. Указатель на что передавать Mathcad'у? Их ведь в управляемом коде как бы и нет.
            +1
            Как-то так, для каждой функции адрес, соответственно свой:
            // Создаёт прокси для функции и возвращает адрес для вызова.
            IntPtr GetFunctionAddress(IFunction function)
            {
                // Создает прокси, где метод Invoke вызовет IFunction.NumericEvaluation и обработает исключения.
                var proxy = ProxyBuilder.GetProxy(function);
                var method = proxy.GetType().GetMethod("Invoke");
                // Создаёт тип делегата с конкретными типами параметров, приправленными [MarshalAs].
                var delegateType = ProxyBuilder.GetDelegateType(method);
                // TODO: Делегат надо закешировать, чтобы GC его раньше времени не прибил.
                var @delegate = Delegate.CreateDelegate(delegateType, proxy, method);
                // Получает адрес неуправляемой прослойки для вызова прокси.
                return Marshal.GetFunctionPointerForDelegate(@delegate);
            }
            


            Я бы и ваш IFunction превратил в просто метод объекта с конкретными типами параметров, из них можно и метаданные вывести. И атрибуты для уточнения.

            Для генерации прокси можно и Linq.Expressions вместо Reflection.Emit использовать, код будет попроще. Делегат создаётся весьма неочевидным образом, к этому надо ещё и генерацию атрибутов добавить.

            Впрочем, на практике я данный метод не использовал, был опыт только с фиксированным набором параметров, там всё в разы проще.
              0
              Да, вот это уже интересней, спасибо. Пока небольшой взрыв мозга. Интересно, этот код будет зависеть от разрядности или нет. Сейчас мне придётся использовать два варианта библиотеки.

              Надо будет протестировать на каком-нибудь простом примере. Осталось только найти простой способ регистрировать функцию во время загрузки сборки.

              На самом деле NumericEvaluation метод и есть, я интерфейс использую для того, чтобы опознавать их в разных сборках. Как иначе их подключать автоматически? Я проверяю каждый класс:

              if ( !type->IsPublic || type->IsAbstract || !IFunction::typeid->IsAssignableFrom( type ) ) continue;
              
              assemblyInfo->Functions->Add( ( IFunction^ ) Activator::CreateInstance( assembly->GetType( type->ToString() ) ) );
              
                0
                А, если будет аналог точки входа в dll, то я могу напрямую вызывать CreateUserFunction. Никакого интерфейса не надо. Описание параметров метода можно брать прямо у класса, описывающего функцию, только тогда они будут задаваться прямо, а не как у меня в виде массива параметров.

                Теперь картинка проясняется. Это сложно, конечно, но со стороны пользователя будет выглядеть проще, чем есть сейчас. У меня все параметры имеют тип Object и нужно их приводить вручную. Хотя, я и сейчас эту идею мог при применить. Думаю, что в c++/cli ведь это тоже доступно. Я просто раньше мало работал с динамическими типами всеми этими и прочими подобными вещами.
              0
              Разве что не уверен, что ModuleInitializer будет корректной заменой DllMain.

              Нашёл где про это написано: Mixed DLL Loading Problem

              В общем и целом, использовать управляемый код внутри DllMain не совсем хорошо, хотя и возможно. Чтобы разделить инициализацию неуправляемого и управляемого кода, рекомендуют использовать managed module initializer, который вызывается сразу после DllMain:

              Proposed Long-Term Solution


              In particular, the common language runtime is adding a new load time event that signals the loading of a module into an application domain. This new event is similar to the native DLL_PROCESS_ATTACH event. When a module is loaded, the common language runtime will check the module for a .cctor method in the global scope. The global .cctor is the managed module initializer. This initializer runs just after the native DllMain (in other words, outside of loader lock) but before any managed code is run or managed data is accessed from that module. The semantics of the module .cctor are very similar to those of class .cctors and are defined in the ECMA C# and Common Language Infrastructure Standards.

              Тут получается сложная ситуация. CreateUserFunction передаёт Mathcad'у также и HITSTANCE модуля, из которого она вызывается. Его можно получить и из DllMain, но я в коде использую GetModuleHandle(NULL). Если в течении загрузки CreateUserFunction не была вызвана, то не освободит ли библиотеку Mathcad?

              В общем, шанс, что такой подход заработает есть, но тут надо экспериментировать. Да, кстати, а доступ к этому самому ModuleInitializer какой? Там нужно будет регистрировать функцию. Можно было бы набросать тестовый пример в указателем и какой-нить простой функцией. Если Mathcad за неё зацепится, то можно будет поработать над идеей дальше.
                +1
                Module Initializer компилятором C# не поддерживается, но его можно задать через IL (удобно для этого использовать Fody).
                Вот только, как оказалось, Module Initializer автоматически после загрузки DLL не вызывается, он выполняется только когда происходит первое обращение к управляемому коду, чего Mathcad, конечно же, делать не будет.

                Вообще я не вижу простого способа зарегистрировать функции, не помещая потенциально опасного кода в нативный DllMain. Разве что на этапе построения сборки с функциями генерировать независимый C++-код для регистрации в DllMain (с захардкоженными FunctionInfo) и линковать его в исходную сборку, как описано здесь. Адреса функций тогда тоже должны быть статическими, для этого прокси тоже надо влинковать в исходную сборку и экспортировать как C-функции.

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

                Если бы Mathcad вызывал экспортированную функцию для инициализации (большинство программ с поддержкой плагинов делают именно так), можно было бы обойтись чисто управляемым кодом, но увы. Видимо, разработчики не предполагали, что для получения адресов из своей же DLL плагину понадобится плясать с бубном.
                  0
                  Ясно, не будем тогда то, что уже хорошо работает, «исправлять» на ещё лучшее :) Ну и мой вариант требует более низкого порога вхождения, чем описанные пляски, если кто разбираться захочет. А вот динамически доставать параметры из метода — это вот идея хорошая. Пользователь тогда не будет привязан к конкретному описанию метода. Почитаю на досуге всю эту заманчивую кухню.
              0
              Хорошая попытка интеграции. Подобная проблема стояла при интеграции 1С и .Net. Старый API 1С удалось подружить, а интеграция с новым вываливается в проблему LoaderLock.
                0
                Спасибо, я сейчас исследую новые возможности и уже вовсю использую новый стиль написания дополнений к Mathcad.

                Думаю, что этот LoaderLock мешал мне использовать Assembly.LoadFrom для разрешения зависимостей. Не знаю насколько удобен будет LoadFile, но он позволял мне загружать дополнительно отдельные сборки (DirectShowLib, к примеру). Хотелось бы, чтобы можно было подключать сторонние математические движки для расширения функционала.

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

                Я тут обновление исходников сделал и добавил интересную возможность вызывать код из другой подключенной сборки. Такого раньше было сделать нельзя.

                Пример с функцией csecho
                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, ref Context context ) {
                
                        //while ( !context.IsUserInterrupted ) { }
                
                        if ( context.IsDefined( "vbecho" ) ) {
                
                            context[ "vbecho" ].NumericEvaluation( args, out result, ref context );
                
                        } else {
                
                            result = Evaluate( ( string ) args[0] );
                        }        
                
                        return true;
                    }
                
                    public string Evaluate( string text ) {
                
                        return text;
                    }
                }
                

              Only users with full accounts can post comments. Log in, please.