Вопрос интеграции уже не раз поднимался на хабре, но, как правило, он посвящен интеграции пары-тройки методов, которые нет возможности реализовать в managed коде. Перед нами же стояла задача взять модуль из C++ и заставить его работать в .NET. Вариант написать заново, по ряду причин, не рассматривался, так что мы приступили к интеграции.
Эта статья не раскрывает всех вопросов интеграции unmanaged модуля в .NET. Есть еще нюансы с передачей строк, логических значений и т.п… По этим вопросам есть документация и несколько статей на хабре, так что здесь эти вопросы не рассматривались.
Стоит отметить, что .NET обёртка на базе Platform Invoke кроссплатформенна, её можно собрать на Mono + gcc.
Интеграция sealed класса
Первое, что приходится осознать при интеграции с помощью Platform Invoke это то, что этот инструмент позволяет интегрировать лишь отдельные функции. Нельзя просто так взять и интегрировать класс. Решение проблемы выглядит просто:
На стороне Unmanaged пишем функцию:
SomeType ClassName_methodName(ClassName * instance, SomeOtherType someArgument)
{
instance->methodName(someArgument);
}
Не забываем к подобным функциям добавить extern «C», чтобы их имена не декорировались C++ компилятором. Это помешало бы нам при интеграции этих функций в .NET.
Далее повторяем процедуру для всех публичных методов класса и интегрируем полученные функции в класс, написанный в .NET. Получившийся класс нельзя наследовать, поэтому в .NET такой класс объявляется как sealed. Как обойти это ограничение и с чем оно связано — смотрите ниже.
А пока вот вам небольшой пример:
Unmanaged class:
class A
{
int mField;
public:
A( int someArgument);
int someMethod( int someArgument);
};
Функции для интеграции:
A * A_createInstance(int someArgument)
{
return new A(someArgument);
}
int A_someMethod(A *instance, int someArgument)
{
return instance->someMethod( someArgument);
}
void A_deleteInstance(A *instance)
{
delete instance;
}
Реализация в .Net:
public sealed class A
{
private IntPtr mInstance;
private bool mDelete;
[ DllImport( "shim.dll", CallingConvention = CallingConvention .Cdecl)]
private static extern IntPtr A_createInstance( int someArgument);
[ DllImport( "shim.dll", CallingConvention = CallingConvention .Cdecl)]
private static extern int A_someMethod( IntPtr instance, int someArgument);
[ DllImport( "shim.dll", CallingConvention = CallingConvention .Cdecl)]
private static extern void A_deleteInstance( IntPtr instance);
internal A( IntPtr instance)
{
Debug.Assert(instance != IntPtr.Zero);
mInstance = instance;
mDelete = false;
}
public A( int someArgument)
{
mInstance = A_createInstance(someArgument);
mDelete = true;
}
public int someMethod( int someArgument)
{
return A_someMethod(mInstance, someArgument);
}
internal IntPtr getUnmanaged()
{
return mInstance;
}
~A()
{
if (mDelete)
A_deleteInstance(mInstance);
}
}
Internal конструктор и метод нужны, чтобы получать экземпляры класса из unmanaged кода и передавать их обратно. Именно с передачей экземпляра класса обратно в unmanaged среду связана проблема наследования. Если класс A отнаследовать в .NET и переопределить ряд его методов (представим, что someMethod объявлен с ключевым словом virtual), мы не сможем обеспечить вызов переопределённого кода из unmanaged среды.
![](https://habrastorage.org/files/079/f21/473/079f2147399f46d4b499f51fe9dd8765.png)
Интеграция интерфейса
Для интеграции интерфейсов нам потребуется обратная связь. Т.е. для полноценного использования интегрируемого модуля нам нужна возможность реализации его интерфейсов. Реализация связана с определением методов в managed среде. Эти методы нужно будет вызывать из unmanaged кода. Тут нам на помощь придут Callback Methods, описанные в документации к Platform Invoke.
На стороне unmanaged среды Callback представляется в виде указателя на функцию:
typedef void (*PFN_MYCALLBACK )();
int _MyFunction(PFN_MYCALLBACK callback);
А в .NET его роль будет играть делегат:
[UnmanagedFunctionPointerAttribute( CallingConvention.Cdecl)]
public delegate void MyCallback ();
[ DllImport("MYDLL.DLL",CallingConvention.Cdecl)]
public static extern void MyFunction( MyCallback callback);
Имея инструмент для обратной связи мы легко сможем обеспечить вызов переопределённых методов.
Но чтобы передать экземпляр реализации интерфейса, в unmanaged среде нам его тоже придётся представить как экземпляр реализации. Так что придётся написать ещё одну реализацию в unmanaged среде. В этой реализации мы, кстати говоря, заложим вызовы Callback функций.
К сожалению, такой подход не позволит нам обойтись без логики в managed интерфейсах, так что нам придётся представить их в виде абстрактных классов. Давайте посмотрим на код:
Unmanaged interface:
class IB
{
public:
virtual int method( int arg) = 0;
virtual ~IB() {};
};
Unmanaged реализация
typedef int (*IB_method_ptr)(int arg);
class UnmanagedB : public IB
{
IB_method_ptr mIB_method_ptr;
public:
void setMethodHandler( IB_method_ptr ptr);
virtual int method( int arg);
//... конструктор/деструктор
};
void UnmanagedB ::setMethodHandler(IB_method_ptr ptr)
{
mIB_method_ptr = ptr;
}
int UnmanagedB ::method(int arg )
{
return mIB_method_ptr( arg);
}
Методы UnmanagedB просто вызывают коллбэки, которые ему выдает managed класс. Здесь нас поджидает еще одна неприятность. До тех пор, пока в unmanaged коде у кого-то есть указатель на UnmanagedB, мы не имеем права удалять экземпляр класса в managed коде, реагирующий на вызов коллбэков. Решению этой проблемы будет посвящена последняя часть статьи.
Функции для интеграции:
UnmanagedB *UnmanagedB_createInstance()
{
return new UnmanagedB();
}
void UnmanagedB_setMethodHandler(UnmanagedB *instance, IB_method_ptr ptr)
{
instance->setMethodHandler( ptr);
}
void UnmanagedB_deleteInstance(UnmanagedB *instance)
{
delete instance;
}
А вот и представление интерфейса в managed коде:
public abstract class AB
{
private IntPtr mInstance;
[DllImport("shim", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr UnmanagedB_createInstance();
[DllImport("shim", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr UnmanagedB_setMethodHandler( IntPtr instance,
[MarshalAs(UnmanagedType.FunctionPtr)] MethodHandler ptr);
[DllImport("shim", CallingConvention = CallingConvention.Cdecl)]
private static extern void UnmanagedB_deleteInstance( IntPtr instance);
[UnmanagedFunctionPointerAttribute( CallingConvention.Cdecl)]
private delegate int MethodHandler( int arg);
private int impl_method( int arg)
{
return method(arg);
}
public abstract int method(int arg);
public AB()
{
mInstance = UnmanagedB_createInstance();
UnmanagedB_setMethodHandler(mInstance, impl_method);
}
~AB()
{
UnmanagedB_deleteInstance(mInstance);
}
internal virtual IntPtr getUnmanaged()
{
return mInstance;
}
}
Каждому методу интерфейса соответствует пара:
- Публичный абстрактный метод, который мы будем переопределять
- «Вызыватель» абстрактного метода (приватный метод с приставкой impl). Может показаться, что он не имеет смысла, но это не так. Этот метод может содержать дополнительные преобразования аргументов и результатов выполнения. Так же в нём может быть заложена дополнительная логика для передачи исключений (как вы уже догадались, просто передать исключение из среды в среду не получится, исключения тоже надо интегрировать)
Вот и всё. Теперь мы можем отнаследовать класс AB и переопределить его метод method. Если нам потребуется передать наследника в unmanaged код мы отдадим вместо него mInstance, который вызовет переопределённый метод через указатель на функцию/делегат. Если же мы получим указатель на интерфейс IB из unmanaged окружения, его потребуется представить в виде экземпляра AB в managed среде. Для этого мы реализуем наследника AB «по умолчанию»:
internal sealed class BImpl : AB
{
[DllImport("shim", CallingConvention = CallingConvention.Cdecl)]
private static extern int BImpl_method( IntPtr instance, int arg);
private IntPtr mInstance;
internal BImpl( IntPtr instance)
{
Debug.Assert(instance != IntPtr.Zero);
mInstance = instance;
}
public override int method(int arg)
{
return BImpl_method(mInstance, arg);
}
internal override IntPtr getUnmanaged()
{
return mInstance;
}
}
Функции для интеграции:
int BImpl_method(IB *instance , int arg )
{
instance->method( arg);
}
По большому счёту это та же интеграция класса без поддержки наследования, описанная выше. Не сложно заметить, что создавая экземпляр BImpl, мы также создаём экземпляр UnmanagedB и делаем не нужные привязки коллбэков. При желании этого можно избежать, но это уже тонкости, здесь мы их описывать не будем.
![](https://habrastorage.org/files/520/6a8/d8d/5206a8d8ddbb424fb520db8fd40cba21.png)
Интеграция классов с поддержкой наследования
Задача — интегрировать класс и предоставить возможность переопределения его методов. Указатель на класс мы будем отдавать в unmanaged, так что надо обеспечить класс коллбэками, чтобы иметь возможность вызвать переопределённые методы.
Рассмотрим класс C, имеющий реализацию в unmanaged коде:
class C
{
public:
virtual int method(int arg);
virtual ~C() {};
};
Для начала мы сделаем вид, что это интерфейс. Интегрируем его также, как это было сделано выше:
Unmanaged наследник для коллбэков:
typedef int (*С_method_ptr )(int arg);
class UnmanagedC : public cpp::C
{
С_method_ptr mС_method_ptr;
public:
void setMethodHandler( С_method_ptr ptr);
virtual int method( int arg);
};
void UnmanagedC ::setMethodHandler(С_method_ptr ptr)
{
mС_method_ptr = ptr;
}
int UnmanagedC ::method(int arg )
{
return mС_method_ptr( arg);
}
Функции для интеграции:
//... опустим методы createInstance и deleteInstance
void UnmanagedC_setMethodHandler(UnmanagedC *instance , С_method_ptr ptr )
{
instance->setMethodHandler( ptr);
}
И реализация в .Net:
public class C
{
private IntPtr mHandlerInstance;
[DllImport("shim", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr UnmanagedC_setMethodHandler( IntPtr instance,
[MarshalAs(UnmanagedType.FunctionPtr)] MethodHandler ptr);
[UnmanagedFunctionPointerAttribute( CallingConvention.Cdecl)]
private delegate int MethodHandler( int arg);
//... также импортируем функции для создания/удаления экземпляра класса
private int impl_method( int arg)
{
return method(arg);
}
public virtual int method(int arg)
{
throw new NotImplementedException();
}
public C()
{
mHandlerInstance = UnmanagedC_createInstance();
UnmanagedC_setMethodHandler(mHandlerInstance, impl_method);
}
~C()
{
UnmanagedC_deleteInstance(mHandlerInstance);
}
internal IntPtr getUnmanaged()
{
return mHandlerInstance;
}
}
Итак, мы можем переопределять метод C.method и он будет корректно вызван из unmanaged среды. Но мы не обеспечили вызов реализации по умолчанию. Здесь нам поможет код из первой части статьи:
Для вызова реализации по умолчанию нам потребуется её интегрировать. Также для её работы нам нужен соответствующий экземпляр класса, который придётся создавать и удалять. Получаем уже знакомый код:
//... опять же опускаем createInstance и deleteInstance
int C_method(C *instance, int arg)
{
return instance->method( arg);
}
Допилим .Net реализацию:
public class C
{
//...
[DllImport("shim", CallingConvention = CallingConvention.Cdecl)]
private static extern int C_method(IntPtr instance, int arg);
public virtual int method(int arg)
{
return C_method(mInstance, arg);
}
public C()
{
mHandlerInstance = UnmanagedC_createInstance();
UnmanagedC_setMethodHandler(mHandlerInstance, impl_method);
mInstance = C_createInstance();
}
~C()
{
UnmanagedC_deleteInstance(mHandlerInstance);
C_deleteInstance(mInstance);
}
//...
}
![](https://habrastorage.org/files/e8e/b9a/b62/e8eb9ab628a4472495afea24ca6ce1b6.png)
Такой класс можно смело применять в managed коде, наследовать, переопределять его методы, передавать указатель на него в unmanaged среду. Даже если мы не переопределяли никаких методов, мы всё равно передадим указатель на UnmanagedC. Это не очень рационально, учитывая, что unmanaged код будет вызывать методы unmanaged класса C транслируя вызовы через managed код. Но такова цена за возможность переопределения методов. В примере, прикреплённом к статье, этот случай продемонстрирован, с помощью вызова метода method у класса D. Если посмотреть на callstack, можно увидеть такую последовательность:
![](https://habrastorage.org/files/6a3/01a/8d1/6a301a8d10634ef8b585cab0a99f06b4.png)
Исключения
Platform Invoke не позволяет передавать исключения и для обхода этой проблемы мы перехватываем все исключения перед переходом из среды в среду, обёртываем информацию об исключении в специальный класс и передаём. На той стороне генерируем исключение на основе полученной информации.
Нам повезло. Наш C++ модуль генерирует только исключения типа ModuleException или его наследников. Так что нам достаточно перехватывать это исключение во всех методах, в которых оно может быть сгенерировано. Чтобы пробросить объект исключения в managed среду нам потребуется интегрировать класс ModuleException. По идее исключение должно содержать текстовое сообщение, но я не хочу заморачиваться с темой маршалинга строк в этой статье, так что в примере будут «коды ошибок»:
public sealed class ModuleException : Exception
{
IntPtr mInstance;
bool mDelete;
//... пропущено create/delete instance
[DllImport("shim", CallingConvention = CallingConvention.Cdecl)]
private static extern int ModuleException_getCode( IntPtr instance);
public int Code
{
get
{
return ModuleException_getCode(mInstance);
}
}
public ModuleException( int code)
{
mInstance = ModuleException_createInstance(code);
mDelete = true;
}
internal ModuleException( IntPtr instance)
{
Debug.Assert(instance != IntPtr.Zero);
mInstance = instance;
mDelete = false;
}
~ModuleException()
{
if (mDelete)
ModuleException_deleteInstance(mInstance);
}
//... пропущено getUnmanaged
}
Теперь предположим, что метод C::method может генерировать исключение ModuleException. Перепишем класс с поддержкой исключений:
//Весь класс описывать не будем, ниже приведены только изменения
typedef int (*С_method_ptr )(int arg, ModuleException **error);
int UnmanagedC ::method(int arg )
{
ModuleException *error = nullptr;
int result = mС_method_ptr( arg, &error);
if (error != nullptr)
{
int code = error->getCode();
//... управление удалением экземпляра error описано ниже и в сэмпле
throw ModuleException(code);
}
return result;
}
int C_method(C *instance, int arg, ModuleException ** error)
{
try
{
return instance->method( arg);
}
catch ( ModuleException& ex)
{
*error = new ModuleException(ex.getCode());
return 0;
}
}
public class C
{
//...
[DllImport("shim", CallingConvention = CallingConvention.Cdecl)]
private static extern int C_method(IntPtr instance, int arg, ref IntPtr error);
[UnmanagedFunctionPointerAttribute( CallingConvention.Cdecl)]
private delegate int MethodHandler( int arg, ref IntPtr error);
private int impl_method( int arg, ref IntPtr error)
{
try
{
return method(arg);
}
catch (ModuleException ex)
{
error = ex.getUnmanaged();
return 0;
}
}
public virtual int method(int arg)
{
IntPtr error = IntPtr.Zero;
int result = C_method(mInstance, arg, ref error);
if (error != IntPtr.Zero)
throw ModuleException(error);
return result;
}
//...
}
Здесь нас тоже ждут неприятности с управлением памятью. В методе impl_method мы передаем указатель на ошибку, но Garbage Collector может удалить её раньше, чем она будет обработана в unmanaged коде. Пора уже разобраться с этой проблемой!
Сборщик мусора против коллбэков
Тут надо сказать, что нам более-менее повезло. Все классы и интерфейсы интегрируемого модуля наследовались от некоего интерфейса IObject, содержащего методы addRef и release. Мы знали, что везде в модуле при передаче указателя производился вызов addRef. И всякий раз, когда потребность в указателе исчезала, производился вызов release. За счёт такого подхода мы легко могли отследить нужен ли указатель unmanaged модулю или колбеки уже можно удалить.
Чтобы избежать удаления managed объектов, используемых в unmanaged среде, нам потребуется менеджер этих объектов. Он будет считать вызовы addRef и release из unmanaged кода и освобождать managed объекты, когда они больше не будут нужны.
Вызовы addRef и release будут пробрасываться из unmanaged кода в managed, так что первое, что нам понадобится — это класс, который обеспечит такой проброс:
typedef long (*UnmanagedObjectManager_remove )(void * instance);
typedef void (*UnmanagedObjectManager_add )(void * instance);
class UnmanagedObjectManager
{
static UnmanagedObjectManager mInstance;
UnmanagedObjectManager_remove mRemove;
UnmanagedObjectManager_add mAdd;
public:
static void add( void *instance);
static long remove( void *instance);
static void setAdd( UnmanagedObjectManager_add ptr);
static void setRemove( UnmanagedObjectManager_remove ptr);
};
UnmanagedObjectManager UnmanagedObjectManager ::mInstance;
void UnmanagedObjectManager ::add(void * instance )
{
if (mInstance.mAdd == nullptr)
return;
mInstance.mAdd( instance);
}
long UnmanagedObjectManager ::remove(void * instance )
{
if (mInstance.mRemove == nullptr)
return 0;
return mInstance.mRemove( instance);
}
void UnmanagedObjectManager ::setAdd(UnmanagedObjectManager_add ptr )
{
mInstance.mAdd = ptr;
}
void UnmanagedObjectManager ::setRemove(UnmanagedObjectManager_remove ptr)
{
mInstance.mRemove = ptr;
}
Второе, что мы должны сделать, это переопределить addRef и release интерфейса IObject так, чтобы они меняли значения счётчика нашего менеджера, хранящегося в managed коде:
template <typename T >
class TObjectManagerObjectImpl : public T
{
mutable bool mManagedObjectReleased;
public:
TObjectManagerObjectImpl()
: mManagedObjectReleased( false)
{
}
virtual ~TObjectManagerObjectImpl()
{
UnmanagedObjectManager::remove(getInstance());
}
void *getInstance() const
{
return ( void *) this;
}
virtual void addRef() const
{
UnmanagedObjectManager::add(getInstance());
}
virtual bool release() const
{
long result = UnmanagedObjectManager::remove(getInstance());
if (result == 0)
if (mManagedObjectReleased)
delete this;
return result == 0;
}
void resetManagedObject() const
{
mManagedObjectReleased = true;
}
};
Теперь классы UnmanagedB и UnmanagedC необходимо отнаследовать от класса TObjectManagerObjectImpl. Рассмотрим на примере UnmanagedC:
class UnmanagedC : public TObjectManagerObjectImpl <C>
{
С_method_ptr mС_method_ptr;
public:
UnmanagedC();
void setMethodHandler( С_method_ptr ptr);
virtual int method( int arg);
virtual ~UnmanagedC();
};
Класс C реализует интерфейс IObject, но теперь методы addRef и release переопределены классом TObjectManagerObjectImpl, так что подсчётом количества указателей будет заниматься менеджер объектов в managed среде.
Пора бы уже взглянуть на код самого менеджера:
internal static class ObjectManager
{
//... импортируем всё, что необходимо, см. сэмпл
private static AddHandler mAddHandler;
private static RemoveHandler mRemoveHandler;
private class Holder
{
internal int count;
internal Object ptr;
}
private static Dictionary< IntPtr, Holder> mObjectMap;
private static long removeImpl( IntPtr instance)
{
return remove(instance);
}
private static void addImpl(IntPtr instance)
{
add(instance);
}
static ObjectManager()
{
mAddHandler = new AddHandler(addImpl);
UnmanagedObjectManager_setAdd(mAddHandler);
mRemoveHandler = new RemoveHandler(removeImpl);
UnmanagedObjectManager_setRemove(mRemoveHandler);
mObjectMap = new Dictionary<IntPtr , Holder >();
}
internal static void add(IntPtr instance, Object ptr = null)
{
Holder holder;
if (!mObjectMap.TryGetValue(instance, out holder))
{
holder = new Holder();
holder.count = 1;
holder.ptr = ptr;
mObjectMap.Add(instance, holder);
}
else
{
if (holder.ptr == null && ptr != null)
holder.ptr = ptr;
holder.count++;
}
}
internal static long remove(IntPtr instance)
{
long result = 0;
Holder holder;
if (mObjectMap.TryGetValue(instance, out holder))
{
holder.count--;
if (holder.count == 0)
mObjectMap.Remove(instance);
result = holder.count;
}
return result;
}
}
Теперь у нас есть менеджер объектов. Перед передачей экземпляра managed объекта в unmanaged среду, мы должны добавить его в менеджер. Так что метод getUnmanaged у классов AB и C необходимо изменить. Приведу код для класса C:
internal IntPtr getUnmanaged()
{
ObjectManager.add(mHandlerInstance, this);
return mHandlerInstance;
}
Теперь мы можем быть уверены, что коллбэки будут работать настолько долго, насколько это необходимо.
Учитывая специфику модуля, потребуется переписать классы, заменив все вызовы ClassName_deleteInstance на вызовы IObject::release, а также не забывать делать IObject::addRef там, где это потребуется. В частности, это позволит избежать преждевременного удаления ModuleException, даже если сборщик мусора удалит managed обёртку, unmanaged экземпляр, будучи наследником IObject, не будет удалён, пока unmanaged модуль не обработает ошибку и не вызовет для неё IObject_release.
Заключение
На самом деле, пока мы занимались интеграцией модуля, мы испытали огромное количество эмоций, выучили немало нецензурных слов и научились спать стоя. Наверно мы должны хотеть, чтобы эта статья кому-нибудь пригодилась, но не дай бог. Конечно решать проблемы управления памятью, наследования и передачи исключений было весело. Но мы интегрировали далеко не три класса и было в них далеко не по одному методу. Это был тест на выносливость.
Если вы, всё же, столкнётесь с такой задачей, то вот вам совет: любите Sublime Text, регулярные выражения и сниппеты. Этот небольшой набор уберёг нас от алкоголизма.
P.S. Рабочий пример интеграции библиотеки доступен по ссылке github.com/simbirsoft-public/pinvoke_example
Комментарии (14)
maydjin
28.01.2016 14:47Проще приготовить assembly с нужными интерфейсами на managed c++. Единственное, что сиё не кроссплатформенно пока что, зато позволяет сгенерировать более производительный биндинг (а зачем ещё плюсы тащить в .Net?)
Вот я бы лучше послушал чей нить succsess story использования swig'a для сложного api.
А так, имхо получилась статья про то, как сделать C биндинг к .Net (притом в рантайме по факту) а потом как на C++ сделать C биндинг (с этой задачей тот же swig точно неплохо справляется, без сниппетов и алкоголя).
alex_blank
28.01.2016 16:11+2Когда-то давно приходилось использовать C++/CLI для подобных целей, писал статью об этом:
habrahabr.ru/post/47732
Но это было аж в 2008, хз, актуально ли это сегодня :)
P.S. лол, с тех пор хабрахабр разучился поддерживать некоторую разметку, такшта статья нечитабельна
Nanako
28.01.2016 16:19Я от таких решений полностью отказался в пользу IPC и отдельных процессов для managed и unmanaged кода. Если managed thread зависает или локается внутри unmanaged call, чтобы организовать ему interrupt или abort пришлось городить отдельную обертку отслеживающую thread native id и грохающую тред на «том конце». В результате еще и память подтекает после «ForcedNativeAbort». Вобщем не рекомендую, но это мое субъективное мнение конечно же.
Nanako
28.01.2016 16:23Ну и да, тем где это действительно было нужно (отладка HLSL кода в WPF, тут код по процессам никак не получится раскидать) я пользовался все же C++/CLI, как выше уже неоднократно заметили это более подходящий инструмент.
Viacheslav01
Я может глупость скажу, но имея полный доступ к коду C++ почему не использовать COM?
maydjin
COM — это лишний уровень коссвености. У вас будет ссылка со счётчиком ссылок на ссылку со счётчиком ссылок.
Плюс, COM тоже не кроссплатформенный(если не брать в рассчёт разные wine и прочее подобное ПО), поэтому c++ clr решение тут будет без всяких «если» разумней.
SSul
Требовалось именно кроссплатформенное решение.
kekekeks
COM крайне криво работает в Mono. Например, GC собирает RCWшки вне зависимости от количества ссылок на них, да и вообще творятся разные непотребства.
ZakharS
Возник такой же вопрос, но уже пояснили, что причиной было требование кроссплатформенности. Однако интересно было бы сравнить производительность этого решения и реализации через COM. У меня сейчас есть C# приложение, которое гоняет огромные объемы данных, получая их через COM. И основное узкое место, судя по профайлеру — это именно маршалинг больших строк. Приложение переписать на C++ сложно, перевести COM-библиотеку на C# не менее сложно. Пока с этим живем, но хочется ускорить.