====== .Net Framework ======
===== Visual C++ CLR Class Library =====
公司內大部分的核心處理元件都是使用 COM DLL 寫成的,而且大部分的情況下也都是用 C++ 讀進去使用。這次由於接到的專案,客戶要求用 C# 做為 UI,呼叫我們的核心處理元件,由於核心處理元件有些複雜與低階,公司不打算讓客戶直接接觸到核心元件,有些功能可能也不打算開給客戶使用,因此希望能先包一層 wrapper 層。
整個架構就差不多是這樣:
* UI - C# Console
* Wrapper - Visuall C++ CLR Class Library
* Kernel - COM DLL
以前其實已經有類似的合作經驗,當時是透過 Visuall C++ ATL Project 來完成的,但是當時傳遞的參數比較簡單,只有基本的 int/float/HRESULT 等等,最複雜也只到 SafeArray,這次則是要直接傳遞圖片的 raw data,處理起來複雜許多。
如果用 ATL 來寫 wrapper 層,預估可能五天還不一定做得玩,寫到第二天的時候發現窒礙難行,決議整個重來,改用 CLR Class Library 寫,兩天就把 UI + wrapper 層全部做完了!
趁記憶還新鮮,紀錄有用到的東西。使用 Visual Studio 2008 以及 .NET Framework 3.5 SP1。
==== Create Project ====
首先建立新的 Solution,選擇 Visual C++ → CLR → Class Library
{{:programming:createclrproject.png?nolink&640|}}
接著建立 C# 的 Console Project,在 Solution Explorer 視窗裡面的 Solution 'CLRTest' 上按滑鼠右鍵 → Add → New Project
{{:programming:createuiproject.png?nolink&640|}}
這邊 UI 的部分看是要選 Console Application 或是 Windows Forms Application 都可以。
接著在 C# 的 Project 上按滑鼠右鍵 -> Add Reference
{{:programming:solutionexplorer_addref.png?nolink|}}
選擇 Project 分頁,然後選擇剛剛建立的 CLR Project
{{:programming:addrefproject.png?nolink|}}
完成後應該就可以在 C# Project 的 References 裡面看到 CLR 的 Project
{{:programming:addref_finished.png?nolink|}}
==== CLR Class Library ====
在完成 Project 的建立之後,就可以開始寫 CLR Class 裡面所需要的功能,像是把 COM DLL 讀進來用,或是寫只有在 C++ 才能用的 [[http://msdn.microsoft.com/en-us/library/windows/desktop/ee719902%28v=vs.85%29.aspx|Windows Imaging Component (WIC)]] 等等。
這邊範例先增加兩個簡單的功能:Add、Minus
CLRTest.h
#pragma once
using namespace System;
namespace CLRTest {
public ref class CLRTestClass
{
public:
int Add(int x, int y);
int Minus(int x, int y);
};
}
CLRTest.cpp
// This is the main DLL file.
#include "stdafx.h"
#include "CLRTest.h"
using namespace CLRTest;
int CLRTestClass::Add(int x, int y)
{
return x + y;
}
int CLRTestClass::Minus(int x, int y)
{
return x - y;
}
完成後即可編譯 CLR Project,生出 Debug\CLRTest.dll 檔案。
這時用 Object Browser 打開 CLR Project,應該可以看到 Class 裡面有兩個 function 可以使用。
{{:programming:objbrowser.png?nolink|}}
==== C# ====
建立好 CLR Project 後,在 C# 就可以很方便的直接拿來用
using System;
using CLRTest;
namespace CLRTestConsole
{
class Program
{
static void Main(string[] args)
{
CLRTestClass clr = new CLRTestClass();
Console.WriteLine("3+2=" + clr.Add(3, 2));
Console.WriteLine("3-2=" + clr.Minus(3, 2));
}
}
}
若編譯錯誤,可能是平台問題,打開 C# Project 的 Properties,在 Build Tab 裡面的 Platform target 選擇 x86
{{:programming:clr_x86.png|}}
==== 進階應用 ====
CLR 的特色之一就是可以直接拿 .Net Framework 的東西來用,因此要和 C# 互通特別容易,像是在 CLR 層將一張圖片檔案透過 GDI+ 打開,接著轉成 Byte Array 給 COM DLL 使用。
例如,COM DLL 內定義一個 buffer pointer 為
BYTE *pBuffer;
在 CLR 層定義需要用到的類別
#using
using namespace System;
using namespace System::Drawing;
using namespace System::Drawing::Imaging;
將放在 path 的圖片打開:
Bitmap^ srcBitmap = gcnew Bitmap(path);
透過 Bitmap.LockBits() 固定住記憶體位址,創建一塊 array 記憶體空間,並將 Bitmap 的內容複製過去,最後用 pin_ptr 取得 pointer address,這樣就可以指定給 pBuffer 了!並且在 LockBits 使用完之後,記得要 UnlockBits。
BitmapData^ srcBitmapData = srcBitmap->LockBits(System::Drawing::Rectangle(0, 0, srcBitmap->Width, srcBitmap->Height), ImageLockMode::ReadWrite, PixelFormat::Format32bppArgb);
IntPtr ptr = srcBitmapData->Scan0;
int bytes = Math::Abs(srcBitmapData->Stride) * srcBitmapData->Height;
array^ rgbValues = gcnew array(bytes);
System::Runtime::InteropServices::Marshal::Copy(ptr, rgbValues, 0, bytes);
pin_ptr p = &rgbValues[0];
Byte* np = p;
pBuffer = np;
// ......
srcBitmap->UnlockBits(srcBitmapData);
另外一個應用就是把 UI 建立好的 Bitmap 傳到 wrapper 層,轉換成 array 後,再傳給 COM DLL 去把圖片放進去,這樣就可以直接把底層修改好的圖片顯示在 UI 上。
CLR 層的寫法是透過 Bitmap^% 取得一個 Bitmap 的 handle,並先產生好一塊 Byte Array 空間,COM DLL 修改完 Byte Array 後,複製到傳入的 Bitmap Handle
關於 **^%** 的用法可參考 [[http://stackoverflow.com/questions/6616599/does-the-symbol-replace-cs-ref-in-parameter-passing-in-c-cli-code|Does the ^ symbol replace C#'s “ref” in parameter passing in C++/CLI code?]]
void CLRTestClass::SetBitmap(int temperature, int tint, Bitmap^% outputBitmap)
{
BitmapData^ oBitmapData = outputBitmap->LockBits(System::Drawing::Rectangle(0, 0, outputBitmap->Width, outputBitmap->Height), ImageLockMode::ReadWrite, PixelFormat::Format32bppArgb);
int bytes = Math::Abs(oBitmapData->Stride) * oBitmapData->Height;
array^ outputBuffer = gcnew array(bytes);
pin_ptr oPin = &outputBuffer[0];
Byte* pOPin = oPin;
// 交由 COM DLL 修改內容...
IntPtr oPtr = oBitmapData->Scan0;
System::Runtime::InteropServices::Marshal::Copy(outputBuffer, 0, oPtr, bytes);
outputBitmap->UnlockBits(oBitmapData);
C# 層的寫法則是先產生好一個 Bitmap,透過 ref Bitmap 的方式傳給 CLR 直接改記憶體內容,完成後即可用 PictureBox 顯示圖片
Bitmap bmPic = new Bitmap(openFileDialog1.FileName);
clImgRetouch.SetWhiteBalance(ref bmPic);
pictureBox1.Image = (Image)bmPic;
==== 參考文件 ====
C# Pass by Reference to CLR
* [[http://msdn.microsoft.com/en-us/library/8903062a%28VS.90%29.aspx|% (Tracking Reference)]]
* [[http://msdn.microsoft.com/en-US/library/yk97tc08%28v=vs.90%29.aspx|^ (Handle to Object on Managed Heap)]]
* [[http://msdn.microsoft.com/en-US/library/te3ecsc8%28v=vs.90%29.aspx|gcnew]]
* [[http://msdn.microsoft.com/en-us/library/1dz8byfh%28v=vs.90%29.aspx|pin_ptr]]
* [[http://bobpowell.net/lockingbits.aspx|Using the LockBits method to access image data]]
WIC
* [[http://msdn.microsoft.com/en-us/library/windows/desktop/ff973956.aspx|Using the Windows Imaging Component]]
COM
* [[http://msdn.microsoft.com/zh-tw/library/aa288455%28v=vs.71%29.aspx|COM Interop 第 1 部份:C# 用戶端教學課程]]
* [[http://www.codeproject.com/Articles/38254/A-Beginner-Tutorial-for-Writing-Simple-COM-ATL-DLL|A Beginner Tutorial for Writing Simple COM/ATL DLL and Using it with .NET]]