2015年8月12日 星期三

[原創] LabVIEW wrapper Callback from dll (3) Example:EnumWindows

最近有網友來信詢問於在LabVIEW中如何使用”User32.dll”中API EnumWindows由於它是使用callback的方式傳回資料因此需封裝成dll的形式給LabVIEW使用,本篇將介紹此過程的程式撰寫方式。
LabVIEW中若想要使用dll來註冊Eventdll必須加入extcode.h這個檔頭和使用Librarylabview.liblabviewv.lib
對於到底是使用labview.liblabviewv.lib,根據這篇文章(labview.lib or labviewv.lib?[http://forums.ni.com/t5/LabVIEW/labview-lib-or-labviewv-lib/td-p/2916894]),我的理解是為了對應不同版本的LabVIEW RunTime,應該要使用labviewv.lib
首先由微軟的MSDN查看EnumWindows的使用說明:
BOOL WINAPI EnumWindows(
  _In_ WNDENUMPROC lpEnumFunc,
  _In_ LPARAM      lParam
);

Parameters

lpEnumFunc [in]
Type: WNDENUMPROC
A pointer to an application-defined callback function. For more information, seeEnumWindowsProc.
lParam [in]
Type: LPARAM
An application-defined value to be passed to the callback function.

Return value

Type:
Type: BOOL
If the function succeeds, the return value is nonzero.
由此可看出EnumWindows需要輸入的參數,lpEnumFunc 就是callback函數的原型,要傳入實作函數程式碼的位址,lParam 則是輸入的參數,要傳入未定的資料型態位址。
接著再看看EnumWindowsProc的函數原型定義:
BOOL CALLBACK EnumWindowsProc(
  _In_ HWND   hwnd,
  _In_ LPARAM lParam
);

Parameters

hwnd [in]
A handle to a top-level window.
lParam [in]
The application-defined value given in EnumWindows or EnumDesktopWindows.

Return value

To continue enumeration, the callback function must return TRUE; to stop enumeration, it must return FALSE.
由此可知EnumWindowsProc這個callback函數的原型宣告方式,當callback發生時系統會提供hwnd就是我們想要列舉的程式handle值,而lParam在這邊並沒有用到。
有了這些資訊就可以開始編寫提供給LabVIEWdll,首先先定義回傳給LabVIEW的資料型態,由於希望取得的是目標程式的類別字串和視窗名稱自串,因此宣告一個結構包和兩個字串:
typedef struct tagStrRec {
          LStrHandle Str1;
          LStrHandle Str2;
} StrRec, *StrRecPtr;
這樣的話在LabVIEW的部分就可以解成cluster包含兩個String的結構
接著宣告全局變數用於儲存LabVIEW UserEvent的位址和常數。
// 紀錄LabVIEW UserEvent的位址
LVUserEventRef EnumWindowsEvent;
// 預設最長字串長度為256 Bytes
#define STRING_LENGHT 256
然後再宣告dll輸出的函數
//用於註冊LabVIEW事件
MgErr __declspec(dllexport) __cdecl RegisterEnumWindowsEvent(LVUserEventRef *value);
//用於呼叫dllEnumerateWindows
void __declspec(dllexport) __cdecl EnumerateWindows(void);
實作這兩個函數與callback函數就完成dll的部分
MgErr __declspec(dllexport) __cdecl RegisterEnumWindowsEvent(LVUserEventRef *value)
{
          //用於紀錄LabVIEW程式內Event的位址
          EnumWindowsEvent = *value;
                    return 1;
}
void __declspec(dllexport) __cdecl EnumerateWindows(void)
{
          //透過dll呼叫EnumWindows,並傳入dll內宣告的callback函數位址。
          EnumWindows(EnumWindowsProc, NULL);
}

BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam)
{
          char class_name[STRING_LENGHT];
          char title[STRING_LENGHT];
          StrRec returnData;
          // init labview string
          returnData.Str1=(LStrHandle)DSNewHandle(sizeof(int32)+STRING_LENGHT*sizeof(uChar));
          returnData.Str2=(LStrHandle)DSNewHandle(sizeof(int32)+STRING_LENGHT*sizeof(uChar));
          // get windows class and title data
          GetClassName(hwnd,class_name, sizeof(class_name));
          GetWindowText(hwnd,title,sizeof(title));
          // convert char arr to labview string
          PopulateStringHandle(returnData.Str1,class_name);
          PopulateStringHandle(returnData.Str2,title);
          // use labview event to send back data
          PostLVUserEvent(EnumWindowsEvent,(void *)&returnData);

          return TRUE;
}

LabVIEW程式的部分主要就是dll的呼叫與User Event的註冊,這邊就直接看圖說故事了




執行後的畫面如下,可以列舉到記事本的類字串與視窗字串:

2014年6月9日 星期一

[原創] LabVIEW wrapper Callback from dll (1) Introduction

LabIEW 的 Call Library Function.vi 提供了介面讓LabVIEW使用者得以呼叫其他語言開發的程式,網路上相關教學資源算是豐富,可以參考下面連結:
官方文件:
How Do I Call a Dynamic Link Library (DLL) from LabVIEW?
Call Library Function Returns the Wrong Function Prototype and Function Parameters for DLLs
Creating Dynamic Link Library (DLL) in Microsoft Visual C++ 6.0 for use in LabVIEW

官方論壇文章:
Calling C/C++ DLLs Containing Simple and Complex Datatypes from LabVIEW
這篇大概解決了90%遇到複雜的structure、pointer的傳遞方式

官方範例程式:
Windows Message Queue Library
這篇應該是最經典範例程式,範例程式教使用者如何用C wrapper LabVIEW內的Message Queue,讓開發人員了解如何打通傳遞dll內的資料到LabVIEW裡面的Queue元件,而且範例程式傳遞的資料不別的正是LabVIEW程式天生缺少的message loop訊息

雖然網路資源如此豐富,但還是缺少了較完整的wrapper callback函數的教學,雖然LAVA有一篇文章我認為已經說明得非常清楚,但對於C/C++/Visual C++相對陌生的LabVIEW開發人員而言還是挺困難的,因此本篇文章會把重心移到如何編寫wrapper callback 函數的部分

第一篇文章會說明callback函數的原理
第二篇文章為實際範例,在dll內掛載全局的鍵盤滑鼠的鉤子,並註冊到user event
這個程式可以讓user event回傳鍵盤滑鼠的資料,不論LabVIEW程式是否為當前視窗都可以,也就是說即使LabVIEW程式被最小化或是隱藏在桌面右下角的圖示,程式還是可以收到鍵盤滑鼠的資料。再來是攔截usb裝置插入/拔出的事件並註冊到user event。不知道為什麼網路上都沒有相關的程式碼資料,難道大家都是用polling偵測usb裝置的有無嗎?用了這個程式後未來靠它就可以知道甚麼時候裝置被插上或拔除了

何謂callback函數呢?
詳細的說明可以參考這篇
實作可以參考這篇
基本上就是我們除了可以用callback讓程式不需要藉由polling的方式等待某些函數的處理,而是等到函數處理完畢後發觸發設定的callback函數然後讓主線程(通常是UI Thread)來處理
例如windows Message傳遞訊息的機制或是driver處理處理與硬體的通訊,都不希望占用到程式執行時需要polling這些"事件",程式需要就去向系統或被呼叫的程式去"註冊"這些事件,一旦發生就轉到註冊好的函數裡執行

在LabVIEW裡的event structure就有點類似這個架構,在LabVIEW裡處理按鈕狀態改變(on cgange),可以靠while loop去polling這個訊息,也可以向event structure去註冊這個訊息,一旦發生就跳到對應的"event程式碼"裡面去處理。按鈕的事件再event程式碼帶出OldVal/NewVal等等按鈕的資料,callback函數也可以帶出函數觸發時需要被處理的參數,因此vc下callback函數的宣告會類似 int (WINAPI *PFCALLBACK)(int Param1,int Param2) ; Param1和Param2就是附帶的相關參數

基於以上所述我認為LabVIEW中最適合wrapper callback函數(所以還有其他方式可以wrapper )接下來教學的內容因此以event structure來wrapper callback

附帶一提,LabVIEW內 Call Library Function Node 裡面的 Callbacks跟本篇文章所談的callback一點關西都沒有

針對這個callbacks的說明參考下面連結:

其中的敘述如下:

Configuring Callbacks

When you configure a Call Library Function Node to call a function, you can use the Callback tab to specify other functions within the same library to call at the following times:
  • Reserve time—When the top-level VI that causes the Call Library Function Node to execute begins executing. Specify a Reserve callback when you need to perform initialization tasks before the primary function executes.
  • Unreserve time—When the top-level VI that caused the Call Library Function Node to execute stops executing. Specify an Unreserve callback when you save or analyze information or carry out clean-up operations.
  • Abort—The specified function executes if the VI that called the primary function aborts.
與就是說如果你想為LabVIEW寫專門的dll,可以寫三個函數Reserve()、Unreserve()、Abort()然後再Callbacks這邊指定,這樣LabVIEW在執行最上層程式碼的時候會自動在開始執行時的初始化階段呼叫Reserve(),在LabVIEW在執行最上層程式碼結束時呼叫Unreserve(),在按下Abort按鈕時執行Abort()所以一般dll應該是沒有機會用上的



[原創] LabVIEW wrapper Callback from dll (2) Example:Keyboard,Mouse,USB event callback

本篇為延續前一篇的內容,進行Callback函數的實作

在LabVIEW中若想要使用dll來註冊Event,dll必須加入extcode.h這個檔頭hextcode.h位於..\National Instruments\(LabVIEW 版本)\cintools目錄下。它定義了CIN和外部程序所用到的基本數據類型和許多函數等。


首先先列出要完成的函數:


// dll 給 LabVIEW呼叫來安裝Mouse Hook的函數
MgErr __declspec(dllexport) InstallMouseHook(LVUserEventRef *value);
// dll 給 LabVIEW呼叫來解除Mouse Hook的函數
MgErr __declspec(dllexport) UnInstallMouseHook(void);

其中LVUserEventRef  * 就是LabVIEW中Event reference傳給dll的資料型態。
MgErr 是NI定義在extcode.h檔頭的錯誤訊息。
實作程式碼非常單純:


MgErr __declspec(dllexport) InstallMouseHook(LVUserEventRef *value)


{

 MgErr bSuccess = noErr;

 userRec.MSEevent = *value;

 if (!ghMouseHook)

 {

  gPreMouseProc = (HOOKPROC) MouseHookProc ;

  bSuccess = (NULL != (ghMouseHook = 

   SetWindowsHookEx (WH_MOUSE_LL, gPreMouseProc,

   g_hInst, NULL)));

 }



 if(bSuccess)

  return noErr;

 else

  return mgArgErr;



}



MgErr __declspec(dllexport) UnInstallMouseHook(void)

{

 if(ghMouseHook != NULL)

 {

  if(UnhookWindowsHookEx(ghMouseHook))

  {

   ghMouseHook=NULL;

   return noErr;

  }else

   return mgArgErr;

 }

 else

  return fNotFound;



}



userRec是用來存放LVUserEventRef  的結構,除此之外都是標準掛載Hook的程式碼。 接下來就是主要提到的Callback函數:
typedef struct tagDataRec {

 LONG lParam;

 LONG wParam;

} DataRec, *DataRecPtr;



LRESULT CALLBACK MouseHookProc 

(int nCode, WPARAM  wParam, LPARAM lParam)

{

 DataRec returnData;

 PMSLLHOOKSTRUCT pmll = (PMSLLHOOKSTRUCT) lParam;

 returnData.lParam=(pmll->pt.x<<16 )+ pmll->pt.y;

 returnData.wParam=wParam;

 PostLVUserEvent(userRec.MSEevent,&returnData);

 return CallNextHookEx (ghMouseHook, nCode, wParam, lParam);



}



DataRec是用來存放當LabVIEW的Event Structure觸發到這個自訂的滑鼠事件時收到的資料型態。程式很單純,把滑鼠座標塞到lParam,把滑鼠按鍵狀態存到wParam,然後透過定義在extcode.h檔頭的函式PostLVUserEvent就將這個事件的消息傳到LabVIEW程式的Event Structure訊息回圈內,一旦程式執行到Event Structure,程式碼就得到這個事件的結果。 LabVIEW端程式碼如下:
範例程式碼內除了攔截滑鼠的資料外,還有鍵盤與USB裝置插拔的資料。


2013年11月27日 星期三

[原創] LabVIEW code security: Password Protect VI v.s. Removing Block Diagram VI

LabVIEW 開發環境提供了兩種方式保護程式原始碼的安全性 , 一個是設定VI的密碼.另一個是移除VI的Block Diagram ,也就是VI的程式邏輯部分.
NI官方網站的說明也非常詳細  可以參考"Security of LabVIEW VI Password Protection vs. Removing VI Block Diagrams" , 雖然此篇文章並沒有明確指出Password Protect VI加密的演算法類型 , 但經實驗已確定是使用了MD5 (Wiki MD5)的加密 , MD5目前已被證實可經由碰撞而產生另一組解 , 因此並非完美的加密方式 , 官方文章也提到 Password Protect VI 的加密流程也可能因LabVIEW本身程式碼被逆向工程,跳過了判斷驗證密碼的程式導致即使加密,卻被輕鬆破解的結果.

因此NI提供了 Removing Block Diagram VI的方式 ,讓該VI 只保存人機介面與編譯過的Binary Code.此方式仍被破解的可能性只有對方能解讀該Binary Code在該平台(x86 ...etc) 對應的邏輯,因效益太低可能性微乎其微,因該不會有人會想做這種事情.此方式的缺點是該VI僅能提供同樣的LabVIEW版本與使用的平台環境,例如 abc.vi是在LabVIEW 2012,x86 Win7底下進行Removing Block Diagram後, abc.vi未來只能在LabVIEW 2012,x86 Win7的環境下被呼叫使用 , 因此若要支援多個版本,則須提供相對應的abc.vi.

Removing Block Diagram VI的流程如下:

1.建立一個vi,範例用一個簡單的加法運算,並加入專案.

2.在專案的Builed Specifications項目右鍵選擇New,然後再選擇Source Distribution.

3.在Source File選項中將目標abc.vi設為Always Included.

4.在source File Settings選項中點到abc.vi , 然後在右邊選單會有個Remove block diagram , 把這個選項打勾.
(後面會再多做一個連Remove front panel打勾的比較)

5. 之後點 Build 按鈕後就會生成不含Block Diagram的vi了. 由上圖可以比較一下
abc.vi為原始vi...... 13KB
abc_RemovingBlackDiagram.vi 為移除Block Diagram.......6KB
abc_RemovAnything.vi 為移除Front Panel與Block Diagram...... 3KB

6.將移除Block Diagram的vi拿到另外一台電腦上用LabVIEW 2013的環境打開就會跳出這個畫面 , 底下紅色框框裡顯說Block Diagram已被移除,因此LabVIEW 2013無法進行轉換.

接著用之前文章 提到的程式稍微修改一下,可以讀取儲存vi的每個部分,簡單說明幾個部分的名字代表的意義(猜的), 
VICD = VI compiled Data , 這是該vi程式碼compile 成machine code的資料.
BDPW = Block Diagram PassWord , 這是該vi被設 Password Protect VI後的密碼生成MD5存放的位置 , 前32 byte 就是該vi 被設密碼後的MD5值 , 若該值為"D41D8CD98F00B204E9800998ECF8427E"代表該vi並未被加密. 後面64 byte可能用來計算vi資料是否遭竄改或毀損(猜的)
FPHb , FPSE 為Front Panel 資料 , 詳細意義不明
BDHb , BDSE 為Block Diagram資料 , 詳細意義不明


上面兩張圖是 abc.vi與 abc_RemovingBlackDiagram.vi 的比較
可以看到VICD並未改變, BDHb , BDSE都被移除.

上面這張圖是 abc_RemovAnything.vi 與 abc_RemovingBlackDiagram.vi 的比較
可以看到VICD並未改變, FPHb , FPSE和其他一些東西都被移除.
結論:
1.Password Protect VI 用MD5加密程式碼.
2.Removing Block Diagram VI無法還原成可編輯的vi
3.就保護程式碼而言Removing Block Diagram VI比Password Protect VI來的安全 , 但使用此vi進行開發非常不方便.

2013年11月13日 星期三

[原創] Constant inside/outside loops

測試For Loop內若使用到常數時,將常數移出迴圈外,程式執行效能是否有所改變.

編譯環境為:
LabVIEW 2013 (32 - bit)
Windows 8 64-bit


第一個程式的比較非常單純 , 迴圈轉1000次,每次累加20,差別只有20放回圈外面或裡面


直接比較binary,可以看出LabVIEW compiler後的machine code是有差別的

看起來差在記憶體位置的位移,使用不同的register做某種資料的運算 , 至於是甚麼運算就去沒有深入了解.到此為止的運算上所需時間是一樣的.



真正的主角是在後面這段 , 迴圈轉1000次 , 每次累加20 (14H) , 比較是否少於1000次,若成立就繼續做
PS. jl (jump on less than, signed)


第二個程式的比較稍微 , 迴圈轉1000次,每次累加20,多做其他運算

Binary的比較,可以看出跑的code size沒變 , 執行運算時間看來是一樣的.


列出所有差異的部分


結論:為了程式碼美觀,常數就放在迴圈裡面吧


2013年11月11日 星期一

[原創] Consider constants for index array...

撰寫 LabVIEW 程式時,若用Index array時常常會拉一堆Index的常數, 這是否會對LabVIEW效能造成影響?

在編譯環境為:
LabVIEW 2013 (32 - bit)
Windows 8 64-bit

比較LabVIEW compiler以下6個程式碼編譯結果的差異


1,2的差別在連接線的常數

3,4的差別是計算和的順序

5,6的差別是,5含有沒用到的常數

比較compile完後各組的machine code差異,差異如下
1與2完全一樣 ,以後拉一堆常數也不用在意了
另外比較一下1和3,看看Index值有改變會怎麼樣,code size長度一樣所以計算時間應該是一樣的,下面有些零散的紅點只是address的變化
3與4改變計算和的順序,大概也是5~7行assembly指令的差別
5與6只差在5的Block Diagram多了不必要的常數加法乘法, LabVIEW compiler果然會無視掉,以後修改程式時可以把舊的算是留著不接,反正沒影響 ...
結論:
Index Array 的常數看來拉多拉少是沒有影響的,程式碼中沒有用到的常數運算看起來也會被編譯器無視掉.

PS.事實上也有另外做過常數擺在迴圈內外的比較,結果是有差異但是編譯後的機械碼長度記得也是一樣的.

2013年11月5日 星期二

[原創] Modified machine code inside .vi file , a proof of how LabVIEW runs in machine code

在了解.vi 檔案內帶有編譯完成machine code之後,可以做一個小小實驗來佐證LabVIEW執行時是直接跑machine code的.考慮簡單的加法運算 255 + 255 =510,透過Code Extractor.vi擷取machine code的部分存檔為"SimpleAdd.vi_255+255.bin";
接著修改為0+255=255,一樣透過Code Extractor.vi擷取machine code的部分存檔為"SimpleAdd.vi_0+255.bin"; 

比對一下"SimpleAdd.vi_255+255.bin"與"SimpleAdd.vi_0+255.bin", 可以發現只差在下圖紅字的地方,其實也可以發現LabVIEW編譯器會直接將兩個常數255+255視為一個常數510(0x01FE)
未來寫code如果遇到有代表意義的常數相加時可以不用客氣直接大辣辣地貼出來,LabVIEW編譯器會幫你自動最佳化成最簡的常數

修改Code Extractor.vi ,讓他可以將修改後的machine code的binary file經zlib加密後取代原始vi檔的machine code部分,並另存檔案修改黨名為Modified_XXX.

開啟修改後的"Modified_ SimpleAdd.vi", Block Diagram雖然顯示 0+255 但執行卻是510
證明了LabVIEW run-time 執行時確實是跑machine code的!
 執行時選用Highlight execution可以看到非常有趣的畫面...

點Ctrl+Shift後再點run button, LabVIEW就會重新編譯回0+255=255了