Опыт использования js-ctypes в Firefox 4 в Windows

Компания, в которой я работаю, занимается разработкой вспомогательных программ для интернет-пользователей. Для вызова этих программ из файрфокса у нас есть специальное расширение. Работает оно очень просто — ищет окно в системе и передает ему данные через WM_COPYDATA. Но для такого взаимодействия с Windows-программами в свое пришлось написать XPCOM компонент на C. В четвертом файрфоксе старые бинарные компоненты перестали работать и одновременно появился механизм для прямой работы с win-библиотеками из яваскрипта — js-ctypes. Пока я переписывал код с C на JS, умудрился нарваться на все подводные камни, которыми теперь и хочу поделиться.
Упрощенная версия кода выглядит так:
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 комментариев

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.