简介
electron使用node-ffi调用windows系统DLL库(user32.dll)中的SendMessageW方法实现发送windows消息至windows窗口。
准备
什么是node-ffi
FFI(Foreign Function Interface)是一种跨语言调用方案,简言之就是我Java写的程序能直接调用你C++写的函数。注意这里是直接调用,而不是我Java进程发送一个消息给C++进程,C++调用某个函数处理我的消息。
node-ffi就是FFI标准在node下的实现
什么是DLL
DLL(Dynamic Link Library)动态链接库是windows系统下的一种共享函数库,可以理解为吧一堆函数打包到一起放在该文件中,供可执行文件动态链接某些函数使用。
node-ffi
我们先来看一个node-ffi官方的例子
var ffi = require('ffi');
// 加载动态库中的ceil取整函数
var libm = ffi.Library('libm', {
'ceil': [ 'double', [ 'double' ] ]
});
// 调用
libm.ceil(1.5); // 2
核心在于ffi.Library()这个方法:
该方法参数分为两部分,第一个参数“libm”是要加载的DLL库名,
第二个参数从左到右依次是 {"函数名":["返回值类型",["参数1类型","参数2类型","参数n类型"]]},对应的就是C++中的函数声明double ceil(double x);
加载完后,ceil便是变量libm的一个方法,可以直接调用。
这里最核心的一点是将C++函数在JS中描述出来,返回值是什么类型,入参是什么类型,由于C++中的类型颇多而且还可以自定义,不可能与JS的类型一一对应,因此需要引入一个库(ref)来帮我们描述C++中的类型。
ref
引入ref库后,我们便可以在JS中描述C++中的基本类型,直接通过ref.types.XXX便可以得到持有一个类型的变量
void
int8
uint8
int16
uint16
int32
uint32
int64
uint64
float
double
Object
CString
bool
byte
char
uchar
short
int
uint
long
ulong
longlong
ulonglong
除此之外,我们还可以在JS中描述指针这个类型,通过ref.refType(类型)便可以得到该类型的指针,举个栗子
const CHAR_P = ref.refType(ref.types.char);
上边这个栗子就得到了char类型的指针,也就是char *
OK,我们现在有了基本类型,有了指针,已经可以描述C++中大部分的类型了,但注意C/C++中还有结构体这种东西,可以将多个类型组合在一起成为新的类型,比如
typedef struct Sample{
int value;
char* name;
} Node;
对于这样的类型,我们还需要一个库的帮助
附:文档地址
ref-struct
该库提供给我们了结构体的类型的支持,直接看官方例子吧,很好理解 C++中的结构体
struct timeval {
time_t tv_sec; /* seconds since Jan. 1, 1970 */
suseconds_t tv_usec; /* and microseconds */
};
对应node中的声明
var ref = require('ref')
// 这个引用一定写对,我就是漏了后边的(ref),导致一直有问题,郁闷了好几天
var StructType = require('ref-struct-di')(ref)
// 定义时间类型
var time_t = ref.types.long
var suseconds_t = ref.types.long
// 定义timeval结构体
var timeval = StructType({
tv_sec: time_t,
tv_usec: suseconds_t
})
// 可以直接new一个实例
var tv = new timeval
windows消息机制
windows消息可以作为一种多进程间的通信方式。当用户点击鼠标、按下键盘都会产生一个特定的消息,放置到应用程序的消息队列中,应用程序过来消费消息,并进行对应的处理。
实际操作
安装依赖
node-ffi
详细步骤:https://github.com/node-ffi-napi/node-ffi-napi
-
node-gyp
npm install -g node-gyp -
windows build tools 戳链接下载安装
https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools勾选 Visual C++ build tools 点击安装,大约4G多 -
node-ffi
npm config set msvs_version 2019
npm install ffi-napi
ref-napi
npm install ref-napi
ref-struct-di
npm install ref-struct-di
ref-wchar-napi
npm install ref-wchar-napi
在JS中使用SendMessageW和FindWindowW
OK,我们已经知道了如何在JS中去调用一个动态库(DLL)中的函数了,接下来让我们看下如何操作发送一个windows消息。
我们可以在这里找到一个叫做SendMessageW的函数,该函数的定义如下
LRESULT SendMessageW(
HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam
);
返回值是LRESULT类型,入参有4个,分别是HWND,UINT,WPARAM,LPARAM类型。
这里可以看到很多类型的定义
LRESULT 的定义是 typedef LONG LRESULT; ,所以我们可以用LONG类型表示LRESULT
HWND 的定义是 DECLARE_HANDLE(HWND);,这里的DECLARE_HANDLE是一个宏
#ifdef STRICT
typedef void *HANDLE;
#define DECLARE_HANDLE() struct name##__ { int unused; }; typedef struct name##__ *name
#else
typedef PVOID HANDLE;
#define DECLARE_HANDLE(name) typedef HANDLE name
#endif
总结下来是一个void *类型的指针。
UINT 没什么好说的ref中有定义
WPARAM 的定义是typedef UINT WPARAM;,也是UINT
LPARAM的定义是typedef LONG LPARAM;,是LONG类型
至此我们已经可以在JS中描述这个函数了。
const FFI = require('ffi-napi');
const ref = require('ref-napi');
const Struct = require('ref-struct-di')(ref);
const LRESULT = ref.types.long;
const POINTER = ref.refType(ref.types.void);
const UINT = ref.types.uint;
const WPARAM = ref.types.uint;
const LPARAM = ref.types.long;
let User32 = new FFI.Library('user32.dll', {
'SendMessageW': [LRESULT, [POINTER, UINT, WPARAM, LPARAM]]
});
在发送之前,我们还需要找到目标窗口的句柄,这里使用到另外一个函数FindWindowW寻找窗口
方法定义如下
HWND FindWindowW(
LPCWSTR lpClassName,
LPCWSTR lpWindowName
);
返回值是HWND类型,参数都是LPCWSTR类型,定义是typedef const wchar_t* LPCWSTR;, 我们使用的ref-wchar-napi提供了该类型。
最后,还有一个隐含的类型, COPYDATA,这是windows的一种消息结构
typedef struct tagCOPYDATASTRUCT {
ULONG_PTR dwData;
DWORD cbData;
PVOID lpData;
} COPYDATASTRUCT, *PCOPYDATASTRUCT;
内含三个属性,分别是ULONG_PTR类型的dwData,为32位的自定义数据,PVOID类型的lpData,是指向数据的指针,DWORD类型的cbData,整形,代表lpData数据的长度。
ULONG_PRT就是ulong的指针类型,PVOID就是void的指针类型,DWORD的定义是typedef unsigned long DWORD,是ulong类型
我们便可以在JS中描述这一数据类型
const COPYDATA = new Struct({
dwData: ULONG_P,
cbData: ULONG,
lpData: VOID_P
});
至此,我们JS部分准备完毕
const FFI = require('ffi-napi');
const ref = require('ref-napi');
const Struct = require('ref-struct-di')(ref);
const ULONG = ref.types.ulong;
const LRESULT = ref.types.long;
const POINTER = ref.refType(ref.types.void);
const UINT = ref.types.uint;
const UINT_P = ref.refType(UINT);
const ULONG_P = ref.refType(ULONG);
const WPARAM = ref.types.uint;
const LPARAM = ref.types.long;
const VOID_P = POINTER;
const CHAR_P = ref.refType(ref.types.char);
const WCHAR_T = require('ref-wchar-napi');
const WCHAR_T_P = ref.refType(WCHAR_T);
const COPYDATA = new Struct({
dwData: ULONG_P,
cbData: ULONG,
lpData: VOID_P
});
let User32 = new FFI.Library('user32.dll', {
'FindWindowW': [POINTER, [WCHAR_T_P, WCHAR_T_P]],
'SendMessageW': [LRESULT, [POINTER, UINT, WPARAM, ref.refType(LPARAM)]]
});
let hwnd = User32.FindWindowW(null, Buffer.from('win32gui\0', 'ucs2'));
let msg = JSON.stringify("测试消息") + '\0';
let length = (new TextEncoder().encode(msg)).length;
data = COPYDATA();
data.dwData = ref.NULL;
data.cbData = length;
data.lpData = ref.allocCString(msg);
User32.SendMessageW(hwnd, 0x004a, null, data.ref())
我们可以直接使用User32.FindWindowW()来寻找窗口句柄,使用User32.SendMessageW()来发送消息
创建一个窗口
直接使用Python创建一个win窗口用来测试
import win32con, win32api, win32gui, ctypes, ctypes.wintypes
from ctypes import *
class COPYDATASTRUCT(Structure):
_fields_ = [('dwData', POINTER(c_uint)),
('cbData', c_uint),
('lpData', c_char_p)]
PCOPYDATASTRUCT = POINTER(COPYDATASTRUCT)
class Listener:
def __init__(self):
message_map = {
win32con.WM_COPYDATA: self.OnCopyData
}
wc = win32gui.WNDCLASS()
wc.lpfnWndProc = message_map
wc.lpszClassName = 'MyWindowClass'
hinst = wc.hInstance = win32api.GetModuleHandle(None)
classAtom = win32gui.RegisterClass(wc)
self.hwnd = win32gui.CreateWindow (
classAtom,
"win32gui",
0,
0,
0,
win32con.CW_USEDEFAULT,
win32con.CW_USEDEFAULT,
0,
0,
hinst,
None
)
print(self.hwnd)
def OnCopyData(self, hwnd, msg, wparam, lparam):
copydata = ctypes.cast(lparam, PCOPYDATASTRUCT).contents
print (copydata.cbData)
data = copydata.lpData[:copydata.cbData - 1]
print (data.decode('utf-8'))
return 1
l = Listener()
win32gui.PumpMessages()
测试
直接运行我们的JS文件,便可以发送一条消息至python窗口,python会打印出消息长度和内容
1181360
15
"测试消息"
15
"测试消息"
15
"测试消息"
31
"中文英文混合消息test"