Как стать автором
Обновить

Комментарии 14

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

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

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

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

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

А как же с динамическим кодом быть? Фишка основная в том, что я «подменяю» неупраляемую callback-функцию, которая имеет переменное количество параметров. Указатель на что передавать Mathcad'у? Их ведь в управляемом коде как бы и нет.
Как-то так, для каждой функции адрес, соответственно свой:
// Создаёт прокси для функции и возвращает адрес для вызова.
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 использовать, код будет попроще. Делегат создаётся весьма неочевидным образом, к этому надо ещё и генерацию атрибутов добавить.

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

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

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

if ( !type->IsPublic || type->IsAbstract || !IFunction::typeid->IsAssignableFrom( type ) ) continue;

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

Теперь картинка проясняется. Это сложно, конечно, но со стороны пользователя будет выглядеть проще, чем есть сейчас. У меня все параметры имеют тип Object и нужно их приводить вручную. Хотя, я и сейчас эту идею мог при применить. Думаю, что в c++/cli ведь это тоже доступно. Я просто раньше мало работал с динамическими типами всеми этими и прочими подобными вещами.
Разве что не уверен, что 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 за неё зацепится, то можно будет поработать над идеей дальше.
Module Initializer компилятором C# не поддерживается, но его можно задать через IL (удобно для этого использовать Fody).
Вот только, как оказалось, Module Initializer автоматически после загрузки DLL не вызывается, он выполняется только когда происходит первое обращение к управляемому коду, чего Mathcad, конечно же, делать не будет.

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

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

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

Публикации

Истории