最近老闆們都去放耶誕節和新年,留守也不會有什麼大事發生,正好可以玩一下想玩的程式概念。在做project的時候由於用到公司別組用C++寫的unmanged程式,免不了得做unmanaged和mamanged的資料交換。C#由於是在.NET架構之下(一般稱為managed code,不在.NET下的程式則稱為unmanged/native code),.NET的資料型別和C++並不相同,尤其是自訂和指標相關物件,如果不額外做點事情,二邊資料彼此是看不懂的,Compiler馬上會抱怨不知道該怎麼辦。要做資料交換,MSDN給三條路:P/Invoke,COM marshaling和C++ marshaling。
P/Invoke和COM marshaling基本上是把native code包成動態程式庫或是COM物件供managed code叫用。效率如何,微軟只說P/Invoke會比COM marshaling好點(additional 10 instructions vs. 50 instructions per data exchange),和全都是managed code比起來慢不少就是了。C++/CLI(早期叫managed C++)算是比較理想的一種做法,Visual Studio會把用C++/CLI寫的程式編成.NET讀得懂的東西(MSIL,C#也是編譯成這個intemediate language),但是在C++/CLI裡面可以直接使用native code寫的物件類別,這個設計提供一個漂亮的資料交換管道,效率也是最好的。只不過要多寫一些程式「騙」.NET說native的資料物件是managed。
假設我有一個寫好的MyArray物件。我想要1)用.NET處理一堆字串物件,但最後要用MyArray物件送到別人用native code寫好的函式裡面去;2)別人處理好的MyArray物件我要用.NET code要做後續的處理:
<template class T>
class MyArray
{
public:
MyArray();
~MyArray();
void add(const T& x);
void remove(const T& x);
bool find(const T& x);
size_t size() const;
const T& operator[](size_t i);
};
解法有很多種,我比較喜歡的是寫一個C++/CLI的集合類別把MyArray<T>包裝起來:
ref class MyDotNetStringCollection: ICollection<String^>
{
private:
MyArray<string>* _array;
internal:
property MyArray* MyArray { MyArray* get() { return _array; } }
public:
MyDotNetStringCollection(): _array(new MyArray<string>) {}
~MyDotNetStringCollection() { this->!MyDotNetStringCollection(); }
!MyDotNetStringCollection() { delete _array; }
#pragma region ICollection<T> members
virtual property int Count { int get() { return (int)_array->size(); } }
virtual property bool IsReadOnly { bool get() { return false; } }
virtual void Add(String^ item)
{
_array->add(marshal_as<string>(item));
}
virtual void Clear()
{
delete _array;
_array = new MyArray<string>;
}
virtual bool Contains(String^ item)
{ return _array->find(marshal_as<string>(item)); }
virtual void CopyTo(array<String^>^ array, int arrayIndex)
{
if(array == nullptr) throw gcnew ArgumentNullException("array");
if(arrayIndex < 0) throw gcnew ArgumentOutOfRangeException("arrayIndex");
int arrayEndIndex = arrayIndex + Count;
if(array->Rank > 1 || arrayEndIndex > array->Length)
throw gcnew ArgumentException();
for(int i = arrayIndex; i < arrayEndIndex; i++)
array[i] = (*_array)[i - arrayIndex];
}
virtual bool Remove(String^ item)
{
string str = marshal_as<string>(item);
if(_array->find(str))
{
_array->remove(str);
return true;
}
return false;
}
#pragma endregion
#pragma region IEnumerable<T> members
virtual IEnumerator<String^>^ GetEnumerator();
virtual IEnumerator IEnumerator_GetEnumerator() = IEnumerator::GetEnumerator
{ return GetEnumerator(); }
#pragma endregion
}
這樣下來,如果我要原來的native MyArray<string>物件就用MyArray Property提出來就可以了,其他的就和一般的.NET Collection完全一樣;如果要用native的函式也行,MyDotNetStringCollection也會隨之更新。裡面留下來沒寫的是GetEnumerator()這個方法,其他的都是很簡單的封裝,最長的CopyTo也只是遵守ICollection<T>::CopyTo的Contract把不合理的引數用例外丟回去而已(.NET 4.0用Code Contract應該會更好,不過還沒時間研究這部份)。marshal_as則是Microsoft提供的基本型別在managed和unmanaged之間的轉換函式。如果是unmanaged的自訂類別,managed code這邊也要定義一個相對應的物件類別做為container,並加上一個自訂的function作為轉換機制。
GetEnumerator()沒先寫下來當然有原因,上一篇有提到C#的yield return會自動提供一個GetEnumerator的實作,可惜C++/CLI沒有這東西,實作要自己生:
ref class MyDotNetStringEnumerator: IEnumerator<String^>
{
private:
MyArray<string>* _array;
int m_index;
int m_count;
public:
MyDotNetStringEnumerator(MyArray<string>* array):
_array(array), m_index(-1), m_count((int)array->size()) {}
~MyDotNetStringEnumerator() { this->!MyDotNetStringEnumerator(); }
!MyDotNetStringEnumerator() { _array = NULL; }
#pragma region IEnumerator<T> members
virtual property String^ Current
{
String^ get()
{
return m_index == -1 ? nullptr :
marshal_as<String^>((*array)[m_index]);
}
}
virtual property Object^ IEnumerator_Current = IEnumerator::Current
{ Object^ get() { return Current; } }
virtual void Reset() { m_index = -1; }
virtual bool MoveNext()
{
if((int)_array->size() != m_count)
throw gcnew InvalidOperationException();
if(m_index == (int)_array->size())
return false;
Interlocked::Increament(m_index);
return true;
}
#pragma endregion
}
IEnumerator<String^>^ MyDotNetStringCollection::GetEnumerator()
{
return gcnew MyDotNetStringEnumerator(_array);
}
很明顯C#的yield return真的幫程式師省了不少工,C++/CLI要寫這麼一大堆才能做同樣的事情。C#提供了foreach(C++/CLI則為for each)關鍵字作為存取IEnumerator<T>的主要方式,底下二個程式片段的功用是完全一樣的。
程式一(MSDN建議的作法):
IEnumerable<String^>^ stringCollection;
for each(String^ str in stringCollection)
{
Console::WriteLine("{0}", str);
}
程式二(實際上內部的運作程式碼):
IEnumerator<String^>^ enumerator = stringCollection->GetEnumerator();
while(enumerator->MoveNext())
{
Console::WriteLine("{0}", enumerator->Current);
}
enumerator->Dispose();
除了資料交換以外,方法或函數也會需要在managed和unmanaged code之間轉換,C++ marshaling在這部份的無痛轉換做到一半,在managed code裡面可以直接呼叫unmanaged function,不用額外做任何事。從unmanaged code叫用managed method就有點麻煩:
using namespace System::Runtime::InteropServices;
typedef void (*MyFuncPtr)(int arg1, int arg2);
ref class MyTest
{
delegate void MyMethod(int arg1, int arg2);
void TestMethod(int arg1, int arg2)
{
// ......
}
void Demo()
{
IntPtr funcPtr = Marshal::GetFunctionPointerForDelegate(
gcnew MyMethod(this, &MyTest::TestMethod));
MyFuncPtr unmanagedFuncPtr = (MyFuncPtr)(void*)funcPtr;
// ......
}
}
Demo這個方法裡面用了Marshal::GetFunctionPointerForDelegate這個方法(在System::Runtime::InteropServices namespace下)把managed delegate轉成函數指標unmanagedFuncPtr,這樣子就可以在unmanaged code裡面使用在managed code裡寫好的程式了。我遇到的情況是別人寫好的C++程式需要一個函數指標當做callback,但是我的程式需要存取managed的資料物件,方法必須要用managed code,所以只好用這種轉換方式把callback送進去。不過這種轉換不要常做,如果常常會用到這個unmanagedFuncPtr這個結果,不如就做一次轉換後把結果存起來,但是相對應的managed delegate物件必須要在unmanagedFuncPtr被叫用時還存在沒被資源回收車收走。得想另外的解決方法,原因是:delegate所佔有的記憶體會因garbage collector移動。資源回收車把不再使用的記憶體回收之後,會移動存活下來的變數在記憶體的位置(把變數和變數中間的位置補滿),否則會有memory fragment的問題,程式如果要大塊連續記憶體可能會要不到。瞭解garbage collector的行為之後,就知道這下子麻煩了,unmanagedFuncPtr是garbage collector看不到的東西,當delegate移動之後,unmanageFuncPtr還是指向原來的地方,執行時就會看到access violation。.NET的標準做法是用gcroot或GCHandle把delegate的記憶體鎖住不讓garbage collector移動,這樣轉成native function就不會有問題,只是會降低garbage collector的效能。我試了一下,在Visual Studio 2010 Debug mode下沒問題,Release mode會出一些狀況,callback在另一個native code執行緒下跑還是會有access denied, previlaged instruction這類OS level的問題,目前還找不到相關資料確定是什麼情況。
這個做法有二個限制:
1. 如果callback是另一個native code的執行緒,TestMethod裡面不能用到任何的managed物件(.NET馬上死掉,抓了好久才抓出來是這個問題,看別人的例子,managed thread沒有這個問題,我自己還沒試)。後來從managed和unmanaged的記憶體分配方式想,這個結果是正常的,unmanaged的程式本來就看不到managed的物件,不出錯才奇怪。那我真的要用managed物件怎麼辦?目前我能想到的是再找個中繼站,也就是說,把managed物件所需要的資訊先用一個unmanaged的data member存起來,轉換的部份用另一個方法做,不要寫在同一個方法裡。把managed物件送進unmanaged function也一樣,先用這個data member把managed物件的資料轉出來,這樣unmanaged function就可以用了(如果物件不大,效率上應該不會比COM差)。如果是這樣的話,那不如把delegate直接改寫成native function,這樣就不存在marshal delegate的問題。這很可能是讓unmanagedFuncPtr長時間存活最有效率的方法。如果delegate是用C#寫的,看起來GCHandle是唯一的做法,如果用C++/CLI,那就直接用native function寫callback吧。
2. 引數型別必須是managed和unmanaged二邊都有的基本型別:像int, float, double和指標void*(.NET是IntPtr)等。這樣就剩下一個問題我到現在還沒搞懂:函數指標的引數如果有自訂物件類別該怎麼辦?當然可以用COM解決(實際上System::Runtime::InteropServices這個namespace主要就是為了COM marshaling設計的),只是我不太喜歡這個解法(太慢太難看)。另外一個可能性是走後門透過void*做轉換。void*對C語言程式師來說應該不是什麼大問題,只是要很小心,void*可以cast成任何一種指標,如果cast成不對的型別,CPU不會馬上說有問題,之後會發生什麼事沒人知道。所以程式可以寫成類似這樣子:
#pragma managed(push, off)
class MyUnmanagedType
{
// .....
};
typedef void (*MyActualCallbackFunc)(const MyUnmanagedType& obj, void* userData);
// function to run
void MyUnmanagedFunction(MyActualCallbackFunc callback, void* userData)
{
MyUnmanagedType obj;
// .......
(*callback)(obj, userData);
}
typedef void (*MyUnmanagedCallbackFunc)(void* obj);
struct MyUserData{ MyUnmanagedCallbackFunc _func; };
// the unmanaged callback to be sent to unmanaged code
void MyCallback(const MyUnmanagedType& obj, void* userData)
{
MyUserData* usrData = (MyUserData*)userData;
(*(usrData->_func))(&obj);
}
#pragma managed(pop)
ref class MyManagedType
{
delegate void MyManagedCallbackDelegate(IntPtr obj);
MyManagedCallbackDelegate^ m_managedCallback;
MyUserData* _usrData;
void ManagedCallback(IntPtr obj)
{
MyUnmanagedType* myObj = (MyUnmanagedType*)(obj.ToPointer());
// ......
}
public:
MyManagedType()
{
m_managedCallback = gcnew MyManagedCallbackDelegate(this, &MyManagedType::ManagedCallback);
_usrData = new MyUserData;
usrData->_func = Marshal::GetFunctionPointerForDelegate(m_managedCallback);
}
void Run()
{
MyUnmanagedFunction(MyCallback, _usrData);
}
~MyManagedType() { this->!MyManagedType(); }
!MyManagedType() { delete _usrData; }
}
相當痛苦而且很容易弄錯,但是為了要用以前留下來的native程式也沒辦法。如果unmanaged callback設計得不好,這些轉換會更複雜,甚至得用到全域變數,在多緒環境之下就更容易有問題了。程式一開始設計爛掉,後面要救都救不回來。