Thứ Năm, 28 tháng 6, 2012

C# – Hướng dẫn cài đặt Low-Level Keyboard Hook (P1)


Để sử dụng hook trong .NET, bạn cần phải làm việc với unmanaged-code hay cụ thể là các Windows API  (Win32 API) gồm: SetWindowsHookEx, CallNextHookEx và UnhookWindowsEx (Xem bài Giới thiệu kĩ thuật Hook và các khái niệm cơ bản). Trong bài viết này, tôi sẽ hướng dẫn cách tạo một ứng dụng hook bàn phím toàn hệ thống.

Ánh xạ kiểu dữ liệu giữa Win32 và .NET

Một vài điểm cần lưu ý khi bạn làm việc với unmanaged-code là việc ánh xạ kiểu dữ liệu giữa Win32 và .NET. Trong Win32 bạn thấy có nhiều kiểu dữ liệu như LRESULT, HWND, HINSTANCE, HHOOK,… thực chất chúng là chỉ là kiểu số nguyên. Chúng được định nghĩa lại cho phù hợp với ý nghĩa và mục đích sử dụng. Trong .NET các kiểu này được ánh xạ tương ứng với kiểu IntPtr. Bạn cũng có thể dùng kiểu int và có thể ép kiểu trực tiếp giữa int và IntPtr.
Tương tự như vậy, con trỏ hàm trong Win32 được ánh xạ tương ứng với delegate. Vậy, với mỗi Hook procedure, bạn cũng phải tạo ra một delegate để làm callback function cho các hàm SetWindowsHookEx và CallNextHookEx.
Một điểm thuận lợi khi dùng các Win32 API trong .NET là bạn có thể tùy chọn kiểu trong khai báo các tham số của hàm. Như bạn có thể thấy trong ví dụ, việc thay thế giữa kiểu int và IntPtr là hợp lệ khi khai báo các hàm như SetWindowsHookEx, UnhookWindowsEx, CallNextHookEx,…

Các Win32 API Function

Sử dụng attribute [DllImport], ta sẽ thêm ba Win32 API cần thiết cho việc sử dụng hook.
01[DllImport("user32.dll",SetLastError=true)]
02private static extern IntPtr SetWindowsHookEx(int idHook,
03    LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
04 
05[DllImport("user32.dll", SetLastError = true)]
06private static extern bool UnhookWindowsHookEx(IntPtr hhk);
07 
08[DllImport("user32.dll", SetLastError = true)]
09private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode,
10    IntPtr wParam, IntPtr lParam);
Tham số SetLastError được đặt bằng true để chúng ta có thể lấy được lỗi trong trường hợp cài đặt hook thất bại.

Các Win32 Structure và Constants

Vì tôi sẽ tạo một ứng dụng hook low level keyboard nên cần phải có hằng số xác định kiểu hook là:
private const int WH_KEYBOARD_LL = 13;
Tiếp đến trong ví dụ này tôi chỉ bắt hai thông điệp KeyUp và KeyDown của bàn phím. Hai thông điệp này có giá trị tương ứng là:
private const int WM_KEYDOWN = 0×0100;
private const int WM_KEYUP = 0×101;
Ngoài ra Win32 còn định nghĩa một structure dùng để lưu thông tin của các sự kiện bàn phím ở mức thấp. Structure này được đặt tên là KBDLLHOOKSTRUCT, tuy nhiên theo chuẩn đặt tên của C#, tôi sẽ đặt một tên rõ ràng hơn cho structure này.
1[StructLayout(LayoutKind.Sequential)]
2public struct KeyboardHookStruct
3{
4    public int VirtualKeyCode;
5    public int ScanCode;
6    public int Flags;
7    public int Time;
8    public int ExtraInfo;
9}
Tham khảo (MSDN): KBDLLHOOKSTRUCT Structure

LowLevelKeyboardProc Callback Function (Hook procedure)

Khi có sự kiện nhấn phím, hàm callback này sẽ được gọi bởi hệ thống. Cú pháp định nghĩa của hàm này có dạng:
LRESULT CALLBACK LowLevelKeyboardProc(
__in  int nCode,
__in  WPARAM wParam,
__in  LPARAM lParam
);
Trong đó:
-       nCode: Xác định hook procedure có thực hiện xử lý không. Nếu giá trị nCode nhỏ hơn 0, hook procedure sẽ bỏ qua xử lý thông điệp. Nếu giá trị bằng 0 (HC_ACTION) có nghĩa là hai tham số wParam  và lParam sẽ chứa thông tin về thông điệp bàn phím.
-      ValueMeaning
HC_ACTION0The wParam and lParam parameters contain information about a keyboard message.
-       wParam: kiểu thông điệp bàn phím, bao gồm: WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, WM_SYSKEYUP.
-       lParam: con trỏ tới  KBDLLHOOKSTRUCT structure.
Trong ví dụ này tôi sẽ định nghĩa hook procedure này như sau:
1private IntPtr KeyboardHookProc(int nCode, IntPtr wParam, IntPtr lParam)
2{
3    if (nCode >= 0)
4    {
5    // do something
6    }
7 
8    return CallNextHookEx(_hookHandle, nCode, wParam, lParam);
9}
Và tạo một delegate tương ứng để làm nhiệm vụ “con trỏ hàm”:
public delegate IntPtr KeyboardHookDelegate(int nCode, IntPtr wParam, IntPtr lParam);

Các điểm cần lưu ý

Lấy handle của tập tin chứa hook procedure
Khi cài đặt global hook, bạn phải có được handle của tập tin PE chứa hook procedure. Trong .NET, tập tin này là một module (một assembly có thể gồm nhiều module). Bạn có thể lấy handle của tập tin bằng cách dùng Win32 API GetModuleHandle, tuy nhiên một cách khác mà .NET hỗ trợ là dùng phương thức static Marshal.GetHINSTANCE().
Dòng lệnh sau cho ta thấy cách để lấy handle của module chính (chứa hook procedure) trong assembly:
IntPtr hInstance = Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().GetModules()[0]);

Nhận thông báo lỗi của Win32
Để lấy được lỗi khi cài đặt hook thất bại, bạn phải tham số SetLastError bằng true trong attribute [DllImport] của hàm API cần bắt lỗi. Khi đó để lấy được lấy mã lỗi, thay vì dùng Win32 API GetLastError ta có thể dùng phương thức static GetLastWin32Error() của lớp System.Runtime.InteropServices.Marshal.
Khi đã có được mã lỗi, thay vì tìm kiếm bảng dò mã lỗi thì bạn có thể dùng lớp Win32Exception trong namespace System.ComponentModel. Constructor của Win32Exception nhận một số chỉ mã lỗi và sẽ chuyển sang thông điệp lỗi tương ứng.
Ví dụ hàm SetWindowsHookEx trả về handle của hook là 0 nếu cài đặt thất bại, bạn có thể viết như sau để xem nguyên nhân của việc thất bại:
if (_hookHandle == IntPtr.Zero)
throw new Win32Exception(Marshal.GetLastWin32Error());

Cài đặt Hook

Phần cài đặt rất đơn giản, tôi tạo một phương thức public Install() để gọi phương thức cài đặt hook thực sự là SetupHook(). Sau khi gọi SetupHook(), phương thức Install() sẽ kiểm tra hook handle và sẽ ném ra một Win32Exception nếu cài đặt hook thất bại. Như bạn thấy phương thức SetupHook() chỉ đơn giản là gọi hàm Win32 API SetWindowsHookEx:
01private KeyboardHookDelegate _hookProc;
02private IntPtr _hookHandle = IntPtr.Zero;
03 
04// ...
05 
06public void Install()
07{
08    _hookProc = KeyboardHookProc;
09    _hookHandle = SetupHook(_hookProc);
10 
11    if (_hookHandle == IntPtr.Zero)
12        throw new Win32Exception(Marshal.GetLastWin32Error());
13}
14 
15private IntPtr SetupHook(KeyboardHookDelegate hookProc)
16{
17    IntPtr hInstance = Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().GetModules()[0]);
18 
19    return SetWindowsHookEx(WH_KEYBOARD_LL, hookProc, hInstance, 0);
20}

Gỡ bỏ Hook

Bạn có thể thấy tôi có vẻ tạo một phương thức dư thừa khi chỉ đơn giản gọi lại một hàm khác. Thực sự phương thức Uninstall() sau chỉ khác nhau về tham số so với hàm UnhookWindowsHookEx:
1public void Uninstall()
2{
3    UnhookWindowsHookEx(_hookHandle);
4}
Phương thức này cũng như phương thức Install() trên được tôi đặt trong một lớp riêng là Y2KeyboardHook. Sẽ an toàn hơn khi bạn tạo hạn chế việc gọi trực tiếp đến các Win32 API do việc truyền tham số sai có thể gây ra nguy hiểm trong một số trường hợp. Việc encapsulation các hàm API cũng giúp cho việc sử dụng lớp bạn tạo ra dễ dàng và thân thiện hơn, như vậy khi sử dụng, bạn không cần phải nhớ tất cả các tham số không cần thiết.
Để bảo đảm việc gỡ bỏ hook được thực hiện, tôi viết thêm một destructor và gọi phương thức Uninstall() trong đó. Destructor sẽ tự động được gọi khi đối tượng bị hủy, bạn không cần phải gọi trực tiếp destructor:
1// destructor
2~Y2KeyboardHook()
3{
4    Uninstall();
5}


Không có nhận xét nào: