Опыт использования js-ctypes в Firefox 4 в Windows
Компания, в которой я работаю, занимается разработкой вспомогательных программ для интернет-пользователей. Для вызова этих программ из файрфокса у нас есть специальное расширение. Работает оно очень просто — ищет окно в системе и передает ему данные через WM_COPYDATA. Но для такого взаимодействия с Windows-программами в свое пришлось написать XPCOM компонент на C. В четвертом файрфоксе старые бинарные компоненты перестали работать и одновременно появился механизм для прямой работы с win-библиотеками из яваскрипта — js-ctypes. Пока я переписывал код с C на JS, умудрился нарваться на все подводные камни, которыми теперь и хочу поделиться.
Упрощенная версия кода выглядит так:
Для большей читабельности примера я его сильно упростил, поэтому код получился не слишком осмысленный. Работающий вариант можно посмотреть, скачав add-on с мозилловского сайта (пока новая версия 1.5 доступна там только как бета).
Упрощенная версия кода выглядит так:
Components.utils.import("resource://gre/modules/ctypes.jsm")
//Первая проблема - разный тип вызовов системных функций для 64 и 32 битных систем. В Firefox-64 надо использовать default_abi, в 32 битной версии default_abi или winapi_abi. Можно было бы везде использовать default_abi, но универсальное объявление калбэк-функций оказалось невозможным. Поэтому пришлось учитывать разрядность браузера
var CallBackType;
var WinABI;
//Чтобы отличить FF64 от FF32, пришлось воспользоваться таким хаком - проверкой размера типа данных size_t, который равен 8 и 4 байтам соответственно
if (ctypes.size_t.size == 8) {
CallBackABI = ctypes.default_abi;
WinABI = ctypes.default_abi;
} else {
CallBackABI = ctypes.stdcall_abi;
WinABI = ctypes.winapi_abi;
}
//грузим dll
var user32dll = ctypes.open('user32.dll');
var kernel32dll = ctypes.open('kernel32.dll');
//объявляем нужные функции
const EnumWindowsProc = ctypes.FunctionType(CallBackABI, ctypes.bool, [ctypes.size_t, ctypes.size_t]);
var EnumWindows = user32dll.declare('EnumWindows', WinABI, ctypes.bool, EnumWindowsProc.ptr, ctypes.size_t);
var SendMessage = user32dll.declare('SendMessageW', WinABI, ctypes.size_t, ctypes.size_t, ctypes.unsigned_int, ctypes.size_t, ctypes.size_t);
//или лучше так, чтобы почти не отличалось от сишной версии:
//const HWND = ctypes.size_t;
//const WPARAM = ctypes.size_t;
//const LPARAM = ctypes.size_t;
//const LRESULT = ctypes.size_t;
//const UINT = ctypes.unsigned_int;
//SendMessage = user32dll.declare('SendMessageW', WinABI, LRESULT, HWND, UINT, WPARAM, LPARAM);
var GetClassName = user32dll.declare('GetClassNameW', WinABI, ctypes.int, ctypes.size_t, ctypes.jschar.ptr, ctypes.int);
var WideCharToMultiByte = kernel32dll.declare('WideCharToMultiByte', WinABI, ctypes.int, ctypes.unsigned_int, ctypes.uint32_t, ctypes.jschar.ptr, ctypes.int, ctypes.char.ptr, ctypes.int, ctypes.char.ptr, ctypes.bool.ptr);
const WM_COPYDATA = 74;
//объявляем структуру для передачи данных с COPYDATA
const COPYDATASTRUCT = new ctypes.StructType('COPYDATASTRUCT',
[ {'dwData': ctypes.uintptr_t},
{'cbData': ctypes.uint32_t},
{'lpData': ctypes.voidptr_t}
]);
//В XPCOM компонентах строки в UTF-16, а в яваскрипте в UTF-8. Из-за уверенности в том, что у меня на входе UTF-16 я долго не мог понять, почему строки не удается перевести в системную кодировку
function Wide2Ansi(str) {
if (str) {
var i = WideCharToMultiByte(CP_ACP, 0, str, -1, null, 0, null, null);
//так выделяется буфер. Первый new создает новый тип - массив заданной длины, а второй - переменную этого типа
var buf = new new ctypes.ArrayType(ctypes.char, i);
i = WideCharToMultiByte(CP_ACP, 0, str, -1, buf, i, null, null);
//из-за того, что в файрфоксе нет восьмибитных строк, мы не преобразуем буфер в строку при помощи buf.readString(), а возвращаем массив как есть
if (i)
return buf;
}
return null;
}
//Это калбэк функция, вызываемая из Windows
function SearchPD(hwnd, lParam) {
var result = true;
var buf = new new ctypes.ArrayType(ctypes.jschar, 255);
GetClassName(hwnd, buf, 255);
if (buf.readString() == 'TMainForm') {
//так можно записать возвращаемое значение в переданную по ссылке переменную
ctypes.size_t.ptr(lParam).contents = hwnd;
result = false;
}
return result;
}
function GetHwnd() {
//создаем враппер для калбэк-функции
var SearchPD_ptr = EnumWindowsProc.ptr(SearchPD);
//создаем переменную, адрес которой будет передан в калбэк-функцию
var wnd = ctypes.size_t(0);
//типизация оказалась строгая, приходится приводить типы в явном виде с ctypes.cast
EnumWindows(SearchPD_ptr, ctypes.cast(wnd.address(), ctypes.size_t));
//а так можно узнать, что было записано в нашу переменную
return wnd.address().contents;
}
var hWnd = GetHwnd();
//Из-за того, что ctypes.size_t может быть 64 битным числом, а яваскрипт напрямую не работает с такими числами, переменные этого типа являются объектами, а не числами и условие if (hWnd) будет выполнятся всегда, независимо от содержимого hWnd. Поэтому проверку на равенство нулю приходится проводить в явном виде
if (hWnd != 0) {
var command = 'hWnd=' + hWnd;
var CD = new COPYDATASTRUCT();
//перед перекодировкой в Ansi неявно перекодируем UTF-8 в UTF-16 при помощи ctypes.jschar.array()
var cmd = Wide2Ansi(ctypes.jschar.array()(command))
if (cmd) {
CD.lpData = cmd.address();
CD.cbData = cmd.length - 1;
CD.dwData = 0;
SendMessage(hWnd, WM_COPYDATA, 0, ctypes.cast(CD.address(), ctypes.size_t));
}
}
Для большей читабельности примера я его сильно упростил, поэтому код получился не слишком осмысленный. Работающий вариант можно посмотреть, скачав add-on с мозилловского сайта (пока новая версия 1.5 доступна там только как бета).
0 комментариев