简介
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"