Monday, 23 August 2010

.Net with unmanaged code P/Invoke. ( Робота з unmanaged кодом за допомогою C#)

Настав час виконати свою обіцянку і поговорити про використання “некерованиго” коду з .NET середовища. Хочу сказати зразу що для роботи з некерованим кодом існує зразу 3 методи – це procedure invoke (p/invoke), com interop і unsafe, але радіти тут не потрібно, тому, що в кожного з них є свої переваги і недоліки, сьогодні я попробую розглянути основний найчастіше використовуваний P/Invoke, інформацю по інших я думаю ви без проблем знайдете в інтернеті.

Чому працювати з unmanaged кодом з C# так важко?

Все діло в тому що з самого початку створення керованого(.NET) коду, на нього були покладені певні обовязки такі як управління памяттю та вивільненням ресурсів, відсутність прямої адресації, потенційно небезпечних вказівників і тд. Це все зробило його набагто легшим в використанні, але й добавило певні недоліки(менша швидкодія, необхідність встановленого .NET Framework…), тому для нормальної роботи некерованого коду його необхідно подавати таким чином щоб він відповідав вимогам керованого. При використанні некерованого коду є певні недоліки – це те, що іноді для використання некерованого коду необхідно прописати доступність операції яку він буде виконувати в маніфесті програми, і якщо в вашому коді є обмежання по роботі з певними ресурсами, використовуючи функцію з некерованого коду ці обмежання можна легко обійти.

Трохи про маршалинг.

Кожному типу змінної в некерованому коді відповідає певна змінна з керованого коду, але прямо вони не є сумісними, тому коли ми передаємо в метод стрічку типу string компілятор автоматично перетворює її(маршалить) на LPTSTR (TCHAR*), але це можна змінити. Дуже хороша стаття по темі маршалінгу і роботи з некерованим кодом є тут а я розгляну лише те що вам найчастіше може знадобитись.

Метод перший P/Invoke.

P/Invoke це спеціальний механізм для виклику функцій з dll. Для цього нам необхідно створити статичний аналог даної функції з тим же набором параметрів і помітити його атрибутом [DLLImport(“Назвафайлу.dll”)] .

using System.Runtime.InteropServices;
public class Win32
{
        [DllImport("User32.Dll")]
        public static extern void SetWindowText(IntPtr hwnd, String lpString);
}

В даному випадку ми викликаємо метод SetWindowText який приймає змінну hwnd типу IntPtr, та стрічку lpString. Тип IntPtr завжди відповідає змінним типу HWND і HMENU, він містить посилання на обєкт для якого ми хочемо виконати дану функцію, тобто якщо ми хочемо викликати цю функцію для поточного вікна то ми повинні в якості параметра передати this.Handle, якщо інший обєкт то відповідно ссилку на його “хендл”. Атрибут [DLLImport(“Назвафайлу.dll”)]  служить для того щоб компілятор знав в якому саме файлі шукати необхідну нам функцію.

Наступним кроком розглянемо виклик функції яка повертає значення типу string – GetWindowText, в неї є вихідний параметр типу  char* і позамовчуванню він нам буде маршалитись на System.String. Та тут виникає одна проблема System.String в нас є незмінним і після його ініціалізації дані в ньому змінити не можна, тому для того щоб мати змогу приймати повернений з функції текст нам необхідно використати класс StringBuilder.

using System.Text;  // для StringBuilder
[DllImport("user32.dll")]
public static extern int GetWindowText(IntPtr hwnd,StringBuilder buf, int nMaxCount);

Тип маршалингу для StringBuilder теж LPTSTR тому ми без проблем зможемо працювати з ним.

StringBuilder sTitleBar = new StringBuilder(255);
GetWindowText(this.Handle, sTitleBar, sTitleBar.Capacity);
MessageBox.Show(sTitleBar.ToString());

В вище наведеному коді ми створюємо новий обєкт StringBuilder – sTitleBar з розмірністю 255. Викликаємо функцію GetWindowText та передаємо в неї параметри: посилання не поточне вікно, змінна для отримання результату виконання, та вказуємо розмір буферу, після чого виводимо результат на екран.

Зміна типу для маршалингу.

Іноді буває необхідно викликати метод передавши йому в якості параметра стрічку в вигляді ANSI а не Unicode який використовується по замовчуванню. Для цього нам необхідно задати відповідний атрибут, розглянемо детальніше на наступному прикладі:

[DllImport("user32.dll")]
public static extern int GetClassName(IntPtr hwnd, [MarshalAs(UnmanagedType.LPStr)] StringBuilder buf, int nMaxCount);

[MarshalAs(UnmanagedType.LPStr)] StringBuilder buf – вказує компілятору що змінна буде передана в вигляді символів ANSI а не Unicode.

Виклик функцій з структурами в якості параметрів.

Для прикладу розглянемо фінкцію GetWindowRect, вона в якості одного з параметру примає структуру в яку записує координати вікна. Щоб передати їй цю структуру ми повинні позначити її атрибутом StructLayout.

[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
    public int left ;
    public int top;
    public int right;
    public int bottom;
}
[DllImport("user32.dll")]
public static extern int GetWindowRect(IntPtr hwnd, ref RECT rc);

Структура в даному випадку передана по ссилці для того щоб функція модифікувала саме ваш обєкт а не його копію. Для використання вище наведеного коду достатньо зробити наступне:

RECT rc = new RECT();
GetWindowRect(this.Handle, ref rc);

Після його виконання наша структура буде заповнена відповідними значеннями.

Висновок, працювати з некерованим кодом важко і незвично але цілком підсилу любому, головне трохи бажання і часу для ознайомлення з основними правилами. Надалі я попробую розширити дану тему, та доповнити новими постами, та через навантеженість на роботі думаю що це буде ще не скоро.

Всім удачі!