quickwin 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/examples/PdfCanvas.js +3 -3
- package/examples/PdfCanvas.tsx +3 -3
- package/examples/pdf_preview2.js +34 -19
- package/examples/pdf_preview2.ts +34 -20
- package/examples/pdf_viewer.js +956 -215
- package/examples/pdf_viewer.tsx +29 -19
- package/examples/test_gallery.js +922 -194
- package/examples/test_react_lazy.js +905 -177
- package/examples/test_react_listbox.js +905 -177
- package/examples/test_root_window.js +905 -177
- package/examples/test_tab.js +905 -177
- package/lib/fetch.js +35 -7
- package/lib/fetch.ts +24 -4
- package/lib/ffi-bind.js +85 -0
- package/lib/ffi-bind.ts +170 -0
- package/lib/ffi-struct.js +224 -0
- package/lib/ffi-struct.ts +239 -0
- package/lib/react-qw/components/DateTimePicker.js +21 -15
- package/lib/react-qw/components/Link.js +4 -6
- package/lib/react-qw/components/ListView.js +84 -40
- package/lib/react-qw/components/PathPicker.js +40 -24
- package/lib/react-qw/components/Tab.js +21 -10
- package/lib/react-qw/components/Tooltip.js +27 -15
- package/lib/react-qw/components/TreeView.js +23 -33
- package/lib/react-qw/nmhdr.js +26 -0
- package/lib/react-qw/nmhdr.ts +32 -0
- package/lib/text-measure.js +14 -24
- package/lib/text-measure.ts +14 -24
- package/lib/win-common-structs.js +39 -0
- package/lib/win-common-structs.ts +41 -0
- package/main.js +46 -0
- package/package.json +2 -1
- package/quickwin.d.ts +7 -2
- package/qwin-nowasm-x86.exe +0 -0
- package/qwin-nowasm.exe +0 -0
- package/qwin-x86.exe +0 -0
- package/qwin.exe +0 -0
- package/test/run.js +3 -0
- package/test/run.ts +3 -0
- package/test/test_ffi.js +12 -8
- package/test/test_ffi.ts +12 -12
- package/test/test_ffi_bind.js +42 -0
- package/test/test_ffi_bind.ts +46 -0
- package/test/test_ffi_struct.js +201 -0
- package/test/test_ffi_struct.ts +223 -0
- package/test/test_net_fetch.js +62 -0
- package/test/test_net_fetch.ts +72 -0
- package/test/test_react_complex.js +908 -180
- package/test/test_react_counter.js +905 -177
- package/test/test_react_flex.js +905 -177
- package/test/test_react_hidden.js +907 -179
- package/test/test_react_listview_link.js +905 -177
- package/test/test_react_render.js +905 -177
- package/test/test_wolfssl_read.js +201 -0
- package/test/test_wolfssl_read.ts +181 -0
package/lib/fetch.js
CHANGED
|
@@ -81,6 +81,10 @@ async function fetchRequest(req) {
|
|
|
81
81
|
let receivedBytes = 0;
|
|
82
82
|
const cleanupSocket = () => {
|
|
83
83
|
state = ST_DONE;
|
|
84
|
+
if (timerId) {
|
|
85
|
+
clearTimeout(timerId);
|
|
86
|
+
timerId = undefined;
|
|
87
|
+
}
|
|
84
88
|
if (ssl) {
|
|
85
89
|
wolfssl.wolfSSL_free(ssl);
|
|
86
90
|
ssl = null;
|
|
@@ -100,12 +104,38 @@ async function fetchRequest(req) {
|
|
|
100
104
|
timerId = undefined;
|
|
101
105
|
}
|
|
102
106
|
};
|
|
107
|
+
const armTimeout = () => {
|
|
108
|
+
if (timerId) {
|
|
109
|
+
clearTimeout(timerId);
|
|
110
|
+
timerId = undefined;
|
|
111
|
+
}
|
|
112
|
+
timerId = setTimeout(() => {
|
|
113
|
+
timerId = undefined;
|
|
114
|
+
if (state === ST_DONE)
|
|
115
|
+
return;
|
|
116
|
+
if (!resolved) {
|
|
117
|
+
doReject(new Error('Request timeout'));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (state === ST_RECV_BODY && stream && _controller) {
|
|
121
|
+
try {
|
|
122
|
+
_controller.error(new Error('Body timeout'));
|
|
123
|
+
}
|
|
124
|
+
catch { /* already closed */ }
|
|
125
|
+
stream = null;
|
|
126
|
+
cleanupSocket();
|
|
127
|
+
}
|
|
128
|
+
}, timeout);
|
|
129
|
+
};
|
|
103
130
|
const doResolve = (response) => {
|
|
104
|
-
if (
|
|
105
|
-
|
|
131
|
+
if (resolved)
|
|
132
|
+
return;
|
|
133
|
+
resolved = true;
|
|
134
|
+
if (state === ST_DONE)
|
|
106
135
|
cleanup();
|
|
107
|
-
|
|
108
|
-
|
|
136
|
+
else
|
|
137
|
+
armTimeout();
|
|
138
|
+
resolve(response);
|
|
109
139
|
};
|
|
110
140
|
const doReject = (error) => {
|
|
111
141
|
if (!resolved) {
|
|
@@ -115,9 +145,7 @@ async function fetchRequest(req) {
|
|
|
115
145
|
reject(error);
|
|
116
146
|
}
|
|
117
147
|
};
|
|
118
|
-
|
|
119
|
-
doReject(new Error('Request timeout'));
|
|
120
|
-
}, timeout);
|
|
148
|
+
armTimeout();
|
|
121
149
|
s = sock.socket();
|
|
122
150
|
if (s < 0) {
|
|
123
151
|
doReject(new Error('Failed to create socket'));
|
package/lib/fetch.ts
CHANGED
|
@@ -107,6 +107,7 @@ async function fetchRequest(req: RequestImpl): Promise<ResponseImpl> {
|
|
|
107
107
|
|
|
108
108
|
const cleanupSocket = (): void => {
|
|
109
109
|
state = ST_DONE
|
|
110
|
+
if (timerId) { clearTimeout(timerId); timerId = undefined }
|
|
110
111
|
if (ssl) { wolfssl.wolfSSL_free(ssl); ssl = null }
|
|
111
112
|
if (ctx) { wolfssl.wolfSSL_CTX_free(ctx); ctx = null }
|
|
112
113
|
if (s !== null && s >= 0) { sock.closesocket(s); s = null }
|
|
@@ -116,17 +117,36 @@ async function fetchRequest(req: RequestImpl): Promise<ResponseImpl> {
|
|
|
116
117
|
if (timerId) { clearTimeout(timerId); timerId = undefined }
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
const armTimeout = (): void => {
|
|
121
|
+
if (timerId) { clearTimeout(timerId); timerId = undefined }
|
|
122
|
+
timerId = setTimeout(() => {
|
|
123
|
+
timerId = undefined
|
|
124
|
+
if (state === ST_DONE) return
|
|
125
|
+
if (!resolved) {
|
|
126
|
+
doReject(new Error('Request timeout'))
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
if (state === ST_RECV_BODY && stream && _controller) {
|
|
130
|
+
try { _controller.error(new Error('Body timeout')) } catch { /* already closed */ }
|
|
131
|
+
stream = null
|
|
132
|
+
cleanupSocket()
|
|
133
|
+
}
|
|
134
|
+
}, timeout)
|
|
135
|
+
}
|
|
136
|
+
|
|
119
137
|
const doResolve = (response: ResponseImpl): void => {
|
|
120
|
-
if (
|
|
138
|
+
if (resolved) return
|
|
139
|
+
resolved = true
|
|
140
|
+
if (state === ST_DONE) cleanup()
|
|
141
|
+
else armTimeout()
|
|
142
|
+
resolve(response)
|
|
121
143
|
}
|
|
122
144
|
|
|
123
145
|
const doReject = (error: Error): void => {
|
|
124
146
|
if (!resolved) { resolved = true; cleanup(); cleanupSocket(); reject(error) }
|
|
125
147
|
}
|
|
126
148
|
|
|
127
|
-
|
|
128
|
-
doReject(new Error('Request timeout'))
|
|
129
|
-
}, timeout)
|
|
149
|
+
armTimeout()
|
|
130
150
|
|
|
131
151
|
s = sock.socket()
|
|
132
152
|
if (s < 0) { doReject(new Error('Failed to create socket')); return }
|
package/lib/ffi-bind.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as ffi from 'ffi';
|
|
2
|
+
import * as win from 'win';
|
|
3
|
+
import './text-codec.js';
|
|
4
|
+
const KIND_TO_FFI = {
|
|
5
|
+
void: ffi.FFI_TYPE_VOID,
|
|
6
|
+
u8: ffi.FFI_TYPE_UINT8,
|
|
7
|
+
i8: ffi.FFI_TYPE_SINT8,
|
|
8
|
+
u16: ffi.FFI_TYPE_UINT16,
|
|
9
|
+
i16: ffi.FFI_TYPE_SINT16,
|
|
10
|
+
u32: ffi.FFI_TYPE_UINT32,
|
|
11
|
+
i32: ffi.FFI_TYPE_SINT32,
|
|
12
|
+
u64: ffi.FFI_TYPE_UINT64,
|
|
13
|
+
i64: ffi.FFI_TYPE_SINT64,
|
|
14
|
+
f32: ffi.FFI_TYPE_FLOAT,
|
|
15
|
+
f64: ffi.FFI_TYPE_DOUBLE,
|
|
16
|
+
ptr: ffi.FFI_TYPE_POINTER,
|
|
17
|
+
wstr: ffi.FFI_TYPE_POINTER,
|
|
18
|
+
};
|
|
19
|
+
// 与类型层 CTypeOf 同步的运行时别名表
|
|
20
|
+
const C_ALIAS = {
|
|
21
|
+
int: 'i32', long: 'i32', short: 'i16', char: 'i8', float: 'f32', double: 'f64',
|
|
22
|
+
DWORD: 'u32', UINT: 'u32', ULONG: 'u32', LONG: 'i32', BOOL: 'i32', HRESULT: 'i32',
|
|
23
|
+
SHORT: 'i16', USHORT: 'u16', BYTE: 'u8',
|
|
24
|
+
LONG_PTR: 'ptr', ULONG_PTR: 'ptr', INT_PTR: 'ptr', UINT_PTR: 'ptr', DWORD_PTR: 'ptr',
|
|
25
|
+
SIZE_T: 'ptr', WPARAM: 'ptr', LPARAM: 'ptr',
|
|
26
|
+
HANDLE: 'ptr', HWND: 'ptr', HDC: 'ptr', HMODULE: 'ptr', HFONT: 'ptr', HBRUSH: 'ptr',
|
|
27
|
+
HICON: 'ptr', HBITMAP: 'ptr', LPVOID: 'ptr', LPCVOID: 'ptr', LPCWSTR: 'ptr',
|
|
28
|
+
PCWSTR: 'ptr', LPWSTR: 'ptr',
|
|
29
|
+
};
|
|
30
|
+
export function normKind(t) {
|
|
31
|
+
const k = C_ALIAS[t] ?? t;
|
|
32
|
+
if (!(k in KIND_TO_FFI))
|
|
33
|
+
throw new Error(`ffi-bind: invalid kind "${t}"`);
|
|
34
|
+
return k;
|
|
35
|
+
}
|
|
36
|
+
const _dllCache = new Map();
|
|
37
|
+
function loadDll(dll) {
|
|
38
|
+
const cached = _dllCache.get(dll);
|
|
39
|
+
if (cached !== undefined)
|
|
40
|
+
return cached;
|
|
41
|
+
const loaded = win.LoadLibrary(dll);
|
|
42
|
+
if (!loaded)
|
|
43
|
+
throw new Error(`ffi-bind: LoadLibrary("${dll}") failed`);
|
|
44
|
+
_dllCache.set(dll, loaded);
|
|
45
|
+
return loaded;
|
|
46
|
+
}
|
|
47
|
+
function parseSig(sig) {
|
|
48
|
+
const parts = sig.split(' -> ');
|
|
49
|
+
const ret = parts[1];
|
|
50
|
+
if (ret === undefined || parts.length > 2) {
|
|
51
|
+
throw new Error(`ffi-bind: invalid signature "${sig}" (expected "arg1 arg2 -> ret")`);
|
|
52
|
+
}
|
|
53
|
+
const retK = normKind(ret);
|
|
54
|
+
if (retK === 'wstr')
|
|
55
|
+
throw new Error('ffi-bind: "wstr" cannot be a return type');
|
|
56
|
+
const tokens = (parts[0] ?? '') === '' ? [] : parts[0].split(' ');
|
|
57
|
+
const args = tokens.map((t) => normKind(t));
|
|
58
|
+
return { args, ret: retK };
|
|
59
|
+
}
|
|
60
|
+
function makeFn(proc, sig) {
|
|
61
|
+
const { args, ret } = parseSig(sig);
|
|
62
|
+
const argTypes = args.map((k) => KIND_TO_FFI[k]);
|
|
63
|
+
const retType = KIND_TO_FFI[ret];
|
|
64
|
+
return (...a) => {
|
|
65
|
+
const vals = args.map((k, i) => k === 'wstr' ? new TextEncoder('utf-16le').encode(String(a[i]) + '\0').buffer : a[i]);
|
|
66
|
+
return ffi.ffiCall(proc, argTypes, vals, retType);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function bind(dll, name, sig) {
|
|
70
|
+
const proc = win.GetProcAddress(loadDll(dll), name);
|
|
71
|
+
if (!proc)
|
|
72
|
+
throw new Error(`ffi-bind: proc "${name}" not found in ${dll}`);
|
|
73
|
+
return makeFn(proc, sig);
|
|
74
|
+
}
|
|
75
|
+
export function bindLib(dll, map) {
|
|
76
|
+
const out = {};
|
|
77
|
+
const h = loadDll(dll);
|
|
78
|
+
for (const [name, sig] of Object.entries(map)) {
|
|
79
|
+
const proc = win.GetProcAddress(h, name);
|
|
80
|
+
if (!proc)
|
|
81
|
+
throw new Error(`ffi-bind: proc "${name}" not found in ${dll}`);
|
|
82
|
+
out[name] = makeFn(proc, sig);
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
package/lib/ffi-bind.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import * as ffi from 'ffi'
|
|
2
|
+
import * as win from 'win'
|
|
3
|
+
import './text-codec.js'
|
|
4
|
+
|
|
5
|
+
// 声明式 FFI 绑定:把字符串签名('ptr wstr i32 ptr i32 -> i32')解析成
|
|
6
|
+
// 一个可调用函数(bind)或一个 dll 的签名表(bindLib)。
|
|
7
|
+
// kind:void u8 i8 u16 i16 u32 i32 u64 i64 f32 f64 ptr wstr
|
|
8
|
+
// 可用 C/Windows typedef 别名:int long short char float double
|
|
9
|
+
// + DWORD UINT LONG BOOL HRESULT ... + *_PTR WPARAM LPARAM SIZE_T
|
|
10
|
+
// + HANDLE HWND HDC ... (+ LPVOID/LPCWSTR 等指针 typedef)
|
|
11
|
+
// 别名在 token 层归一化到上面规范 kind(见 CTypeOf / C_ALIAS,两者须同步)
|
|
12
|
+
// ptr 收 ArrayBuffer | number | null,槽宽=指针宽(ia32 4B / x64 8B)
|
|
13
|
+
// wstr 收 string,自动 utf-16le+'\0' 编码后走 ptr 槽(仅参数,不能作返回)
|
|
14
|
+
|
|
15
|
+
type Kind = 'void' | 'u8' | 'i8' | 'u16' | 'i16' | 'u32' | 'i32' | 'u64' | 'i64' | 'f32' | 'f64' | 'ptr' | 'wstr'
|
|
16
|
+
|
|
17
|
+
type ArgTypeOf = {
|
|
18
|
+
u8: number; i8: number; u16: number; i16: number
|
|
19
|
+
u32: number; i32: number
|
|
20
|
+
u64: number; i64: number
|
|
21
|
+
f32: number; f64: number
|
|
22
|
+
ptr: ArrayBuffer | number | null
|
|
23
|
+
wstr: string
|
|
24
|
+
}
|
|
25
|
+
type RetTypeOf = {
|
|
26
|
+
u8: number; i8: number; u16: number; i16: number
|
|
27
|
+
u32: number; i32: number
|
|
28
|
+
u64: number; i64: number
|
|
29
|
+
f32: number; f64: number
|
|
30
|
+
ptr: number | null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// C / Windows typedef → 规范 kind。Windows x86/x64 均 LLP64:int/long 恒 32 位,
|
|
34
|
+
// long long 恒 64 位;LONG_PTR/WPARAM/SIZE_T 等指针宽随 arch 走 'ptr' 槽。
|
|
35
|
+
// 运行时见 C_ALIAS(两表内容必须保持一致)。
|
|
36
|
+
type CTypeOf = {
|
|
37
|
+
int: 'i32'
|
|
38
|
+
long: 'i32'
|
|
39
|
+
short: 'i16'
|
|
40
|
+
char: 'i8'
|
|
41
|
+
float: 'f32'
|
|
42
|
+
double: 'f64'
|
|
43
|
+
DWORD: 'u32'
|
|
44
|
+
UINT: 'u32'
|
|
45
|
+
ULONG: 'u32'
|
|
46
|
+
LONG: 'i32'
|
|
47
|
+
BOOL: 'i32'
|
|
48
|
+
HRESULT: 'i32'
|
|
49
|
+
SHORT: 'i16'
|
|
50
|
+
USHORT: 'u16'
|
|
51
|
+
BYTE: 'u8'
|
|
52
|
+
LONG_PTR: 'ptr'
|
|
53
|
+
ULONG_PTR: 'ptr'
|
|
54
|
+
INT_PTR: 'ptr'
|
|
55
|
+
UINT_PTR: 'ptr'
|
|
56
|
+
DWORD_PTR: 'ptr'
|
|
57
|
+
SIZE_T: 'ptr'
|
|
58
|
+
WPARAM: 'ptr'
|
|
59
|
+
LPARAM: 'ptr'
|
|
60
|
+
HANDLE: 'ptr'
|
|
61
|
+
HWND: 'ptr'
|
|
62
|
+
HDC: 'ptr'
|
|
63
|
+
HMODULE: 'ptr'
|
|
64
|
+
HFONT: 'ptr'
|
|
65
|
+
HBRUSH: 'ptr'
|
|
66
|
+
HICON: 'ptr'
|
|
67
|
+
HBITMAP: 'ptr'
|
|
68
|
+
LPVOID: 'ptr'
|
|
69
|
+
LPCVOID: 'ptr'
|
|
70
|
+
LPCWSTR: 'ptr'
|
|
71
|
+
PCWSTR: 'ptr'
|
|
72
|
+
LPWSTR: 'ptr'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type Norm<T extends string> = T extends keyof CTypeOf ? CTypeOf[T] : T
|
|
76
|
+
|
|
77
|
+
type ArgsOf<T extends string> =
|
|
78
|
+
T extends '' ? []
|
|
79
|
+
: T extends `${infer H} ${infer R}` ? [ArgTypeOf[Norm<H> & keyof ArgTypeOf], ...ArgsOf<R>]
|
|
80
|
+
: [ArgTypeOf[Norm<T> & keyof ArgTypeOf]]
|
|
81
|
+
|
|
82
|
+
type _Args<S extends string> = S extends `${infer A} -> ${string}` ? ArgsOf<A> : never
|
|
83
|
+
type _Ret<S extends string> = S extends `${string} -> ${infer R}` ? RetTypeOf[Norm<R> & keyof RetTypeOf] : never
|
|
84
|
+
|
|
85
|
+
const KIND_TO_FFI: Record<Kind, ffi.FfiType> = {
|
|
86
|
+
void: ffi.FFI_TYPE_VOID,
|
|
87
|
+
u8: ffi.FFI_TYPE_UINT8,
|
|
88
|
+
i8: ffi.FFI_TYPE_SINT8,
|
|
89
|
+
u16: ffi.FFI_TYPE_UINT16,
|
|
90
|
+
i16: ffi.FFI_TYPE_SINT16,
|
|
91
|
+
u32: ffi.FFI_TYPE_UINT32,
|
|
92
|
+
i32: ffi.FFI_TYPE_SINT32,
|
|
93
|
+
u64: ffi.FFI_TYPE_UINT64,
|
|
94
|
+
i64: ffi.FFI_TYPE_SINT64,
|
|
95
|
+
f32: ffi.FFI_TYPE_FLOAT,
|
|
96
|
+
f64: ffi.FFI_TYPE_DOUBLE,
|
|
97
|
+
ptr: ffi.FFI_TYPE_POINTER,
|
|
98
|
+
wstr: ffi.FFI_TYPE_POINTER,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 与类型层 CTypeOf 同步的运行时别名表
|
|
102
|
+
const C_ALIAS: Record<string, Kind> = {
|
|
103
|
+
int: 'i32', long: 'i32', short: 'i16', char: 'i8', float: 'f32', double: 'f64',
|
|
104
|
+
DWORD: 'u32', UINT: 'u32', ULONG: 'u32', LONG: 'i32', BOOL: 'i32', HRESULT: 'i32',
|
|
105
|
+
SHORT: 'i16', USHORT: 'u16', BYTE: 'u8',
|
|
106
|
+
LONG_PTR: 'ptr', ULONG_PTR: 'ptr', INT_PTR: 'ptr', UINT_PTR: 'ptr', DWORD_PTR: 'ptr',
|
|
107
|
+
SIZE_T: 'ptr', WPARAM: 'ptr', LPARAM: 'ptr',
|
|
108
|
+
HANDLE: 'ptr', HWND: 'ptr', HDC: 'ptr', HMODULE: 'ptr', HFONT: 'ptr', HBRUSH: 'ptr',
|
|
109
|
+
HICON: 'ptr', HBITMAP: 'ptr', LPVOID: 'ptr', LPCVOID: 'ptr', LPCWSTR: 'ptr',
|
|
110
|
+
PCWSTR: 'ptr', LPWSTR: 'ptr',
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function normKind(t: string): Kind {
|
|
114
|
+
const k = C_ALIAS[t] ?? t
|
|
115
|
+
if (!(k in KIND_TO_FFI)) throw new Error(`ffi-bind: invalid kind "${t}"`)
|
|
116
|
+
return k as Kind
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const _dllCache: Map<string, win.HMODULE> = new Map()
|
|
120
|
+
|
|
121
|
+
function loadDll(dll: string): win.HMODULE {
|
|
122
|
+
const cached = _dllCache.get(dll)
|
|
123
|
+
if (cached !== undefined) return cached
|
|
124
|
+
const loaded = win.LoadLibrary(dll)
|
|
125
|
+
if (!loaded) throw new Error(`ffi-bind: LoadLibrary("${dll}") failed`)
|
|
126
|
+
_dllCache.set(dll, loaded)
|
|
127
|
+
return loaded
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parseSig(sig: string): { args: Kind[]; ret: Kind } {
|
|
131
|
+
const parts = sig.split(' -> ')
|
|
132
|
+
const ret = parts[1]
|
|
133
|
+
if (ret === undefined || parts.length > 2) {
|
|
134
|
+
throw new Error(`ffi-bind: invalid signature "${sig}" (expected "arg1 arg2 -> ret")`)
|
|
135
|
+
}
|
|
136
|
+
const retK = normKind(ret)
|
|
137
|
+
if (retK === 'wstr') throw new Error('ffi-bind: "wstr" cannot be a return type')
|
|
138
|
+
const tokens = (parts[0] ?? '') === '' ? [] : (parts[0] as string).split(' ')
|
|
139
|
+
const args = tokens.map((t) => normKind(t))
|
|
140
|
+
return { args, ret: retK }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function makeFn(proc: number, sig: string): (...a: unknown[]) => unknown {
|
|
144
|
+
const { args, ret } = parseSig(sig)
|
|
145
|
+
const argTypes = args.map((k) => KIND_TO_FFI[k])
|
|
146
|
+
const retType = KIND_TO_FFI[ret]
|
|
147
|
+
return (...a: unknown[]) => {
|
|
148
|
+
const vals = args.map((k, i) =>
|
|
149
|
+
k === 'wstr' ? new TextEncoder('utf-16le').encode(String(a[i]) + '\0').buffer : a[i])
|
|
150
|
+
return ffi.ffiCall(proc, argTypes as any, vals as any, retType)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function bind<const S extends string>(dll: string, name: string, sig: S): (...args: _Args<S>) => _Ret<S> {
|
|
155
|
+
const proc = win.GetProcAddress(loadDll(dll), name)
|
|
156
|
+
if (!proc) throw new Error(`ffi-bind: proc "${name}" not found in ${dll}`)
|
|
157
|
+
return makeFn(proc, sig) as unknown as (...args: _Args<S>) => _Ret<S>
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function bindLib<const M extends Record<string, string>>(dll: string, map: M):
|
|
161
|
+
{ [K in keyof M]: (...args: _Args<M[K]>) => _Ret<M[K]> } {
|
|
162
|
+
const out: Record<string, (...a: unknown[]) => unknown> = {}
|
|
163
|
+
const h = loadDll(dll)
|
|
164
|
+
for (const [name, sig] of Object.entries(map)) {
|
|
165
|
+
const proc = win.GetProcAddress(h, name)
|
|
166
|
+
if (!proc) throw new Error(`ffi-bind: proc "${name}" not found in ${dll}`)
|
|
167
|
+
out[name] = makeFn(proc, sig)
|
|
168
|
+
}
|
|
169
|
+
return out as { [K in keyof M]: (...args: _Args<M[K]>) => _Ret<M[K]> }
|
|
170
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import * as os from 'os';
|
|
2
|
+
import { normKind } from './ffi-bind.js';
|
|
3
|
+
const PTR = os.arch === 'x64' ? 8 : 4;
|
|
4
|
+
// kind → [align, size]
|
|
5
|
+
const AS = {
|
|
6
|
+
u8: [1, 1], i8: [1, 1],
|
|
7
|
+
u16: [2, 2], i16: [2, 2],
|
|
8
|
+
u32: [4, 4], i32: [4, 4],
|
|
9
|
+
u64: [8, 8], i64: [8, 8],
|
|
10
|
+
f32: [4, 4], f64: [8, 8],
|
|
11
|
+
ptr: [PTR, PTR],
|
|
12
|
+
wstr: [2, 2], // 仅定长数组元素用(wchar)
|
|
13
|
+
};
|
|
14
|
+
function alignUp(v, a) {
|
|
15
|
+
return (v + a - 1) & ~(a - 1);
|
|
16
|
+
}
|
|
17
|
+
function parseField(f) {
|
|
18
|
+
const arr = /^(.+)\[(\d+)\]$/.exec(f);
|
|
19
|
+
if (arr) {
|
|
20
|
+
const b = normKind(arr[1]);
|
|
21
|
+
if (b === 'void')
|
|
22
|
+
throw new Error(`ffi-struct: "void" cannot be an array base`);
|
|
23
|
+
return { base: b, count: Number(arr[2]) };
|
|
24
|
+
}
|
|
25
|
+
const b = normKind(f);
|
|
26
|
+
if (b === 'void' || b === 'wstr')
|
|
27
|
+
throw new Error(`ffi-struct: invalid scalar field type "${f}"`);
|
|
28
|
+
return { base: b, count: 1 };
|
|
29
|
+
}
|
|
30
|
+
function layout(def) {
|
|
31
|
+
const fields = [];
|
|
32
|
+
let cursor = 0;
|
|
33
|
+
let maxAlign = 1;
|
|
34
|
+
for (const key of Object.keys(def)) {
|
|
35
|
+
const v = def[key];
|
|
36
|
+
let a;
|
|
37
|
+
let w;
|
|
38
|
+
if (typeof v === 'string') {
|
|
39
|
+
const { base, count } = parseField(v);
|
|
40
|
+
const [align, elem] = AS[base];
|
|
41
|
+
a = align;
|
|
42
|
+
w = count >= 1 ? elem * count : elem;
|
|
43
|
+
fields.push({ name: key, base, count, offset: 0 });
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const child = v;
|
|
47
|
+
a = child.structAlign;
|
|
48
|
+
w = child.size;
|
|
49
|
+
fields.push({ name: key, base: 'u8', count: 0, offset: 0, child });
|
|
50
|
+
}
|
|
51
|
+
const off = alignUp(cursor, a);
|
|
52
|
+
const f = fields[fields.length - 1];
|
|
53
|
+
f.offset = off;
|
|
54
|
+
cursor = off + w;
|
|
55
|
+
if (a > maxAlign)
|
|
56
|
+
maxAlign = a;
|
|
57
|
+
}
|
|
58
|
+
return { fields, size: alignUp(cursor, maxAlign), structAlign: maxAlign };
|
|
59
|
+
}
|
|
60
|
+
const _layoutCache = new WeakMap();
|
|
61
|
+
function buildLayout(def) {
|
|
62
|
+
let hit = _layoutCache.get(def);
|
|
63
|
+
if (hit)
|
|
64
|
+
return hit;
|
|
65
|
+
hit = layout(def);
|
|
66
|
+
_layoutCache.set(def, hit);
|
|
67
|
+
return hit;
|
|
68
|
+
}
|
|
69
|
+
function readScalar(dv, off, k) {
|
|
70
|
+
switch (k) {
|
|
71
|
+
case 'u8': return dv.getUint8(off);
|
|
72
|
+
case 'i8': return dv.getInt8(off);
|
|
73
|
+
case 'u16': return dv.getUint16(off, true);
|
|
74
|
+
case 'i16': return dv.getInt16(off, true);
|
|
75
|
+
case 'u32': return dv.getUint32(off, true);
|
|
76
|
+
case 'i32': return dv.getInt32(off, true);
|
|
77
|
+
case 'u64': return Number(dv.getBigUint64(off, true));
|
|
78
|
+
case 'i64': return Number(dv.getBigInt64(off, true));
|
|
79
|
+
case 'f32': return dv.getFloat32(off, true);
|
|
80
|
+
case 'f64': return dv.getFloat64(off, true);
|
|
81
|
+
case 'ptr': return readPtr(dv, off);
|
|
82
|
+
case 'wstr': return dv.getUint16(off, true);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function readPtr(dv, off) {
|
|
86
|
+
if (PTR === 8)
|
|
87
|
+
return Number(dv.getBigUint64(off, true));
|
|
88
|
+
return dv.getUint32(off, true);
|
|
89
|
+
}
|
|
90
|
+
function writeScalar(dv, off, k, val) {
|
|
91
|
+
switch (k) {
|
|
92
|
+
case 'u8':
|
|
93
|
+
dv.setUint8(off, val);
|
|
94
|
+
break;
|
|
95
|
+
case 'i8':
|
|
96
|
+
dv.setInt8(off, val);
|
|
97
|
+
break;
|
|
98
|
+
case 'u16':
|
|
99
|
+
dv.setUint16(off, val, true);
|
|
100
|
+
break;
|
|
101
|
+
case 'i16':
|
|
102
|
+
dv.setInt16(off, val, true);
|
|
103
|
+
break;
|
|
104
|
+
case 'u32':
|
|
105
|
+
dv.setUint32(off, val >>> 0, true);
|
|
106
|
+
break;
|
|
107
|
+
case 'i32':
|
|
108
|
+
dv.setInt32(off, val | 0, true);
|
|
109
|
+
break;
|
|
110
|
+
case 'u64':
|
|
111
|
+
dv.setBigUint64(off, BigInt(Math.trunc(val)), true);
|
|
112
|
+
break;
|
|
113
|
+
case 'i64':
|
|
114
|
+
dv.setBigInt64(off, BigInt(Math.trunc(val)), true);
|
|
115
|
+
break;
|
|
116
|
+
case 'f32':
|
|
117
|
+
dv.setFloat32(off, val, true);
|
|
118
|
+
break;
|
|
119
|
+
case 'f64':
|
|
120
|
+
dv.setFloat64(off, val, true);
|
|
121
|
+
break;
|
|
122
|
+
case 'ptr':
|
|
123
|
+
writePtr(dv, off, val);
|
|
124
|
+
break;
|
|
125
|
+
case 'wstr':
|
|
126
|
+
dv.setUint16(off, val, true);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function writePtr(dv, off, val) {
|
|
131
|
+
if (PTR === 8)
|
|
132
|
+
dv.setBigUint64(off, BigInt(Math.trunc(val)), true);
|
|
133
|
+
else
|
|
134
|
+
dv.setUint32(off, val >>> 0, true);
|
|
135
|
+
}
|
|
136
|
+
function readField(dv, base, f) {
|
|
137
|
+
const off = base + f.offset;
|
|
138
|
+
if (f.child)
|
|
139
|
+
return readInto(f.child, dv.buffer, off);
|
|
140
|
+
if (f.count > 1) {
|
|
141
|
+
if (f.base === 'wstr') {
|
|
142
|
+
const arr = new Uint16Array(dv.buffer, dv.byteOffset + off, f.count);
|
|
143
|
+
let end = 0;
|
|
144
|
+
while (end < arr.length && arr[end] !== 0)
|
|
145
|
+
end++;
|
|
146
|
+
let s = '';
|
|
147
|
+
for (let i = 0; i < end; i++)
|
|
148
|
+
s += String.fromCharCode(arr[i]);
|
|
149
|
+
return s;
|
|
150
|
+
}
|
|
151
|
+
const out = [];
|
|
152
|
+
const step = AS[f.base][1];
|
|
153
|
+
for (let i = 0; i < f.count; i++)
|
|
154
|
+
out.push(readScalar(dv, off + i * step, f.base));
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
return readScalar(dv, off, f.base);
|
|
158
|
+
}
|
|
159
|
+
function writeField(dv, base, f, v) {
|
|
160
|
+
const off = base + f.offset;
|
|
161
|
+
if (f.child) {
|
|
162
|
+
writeInto(f.child, v, dv.buffer, off);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (f.count > 1) {
|
|
166
|
+
if (f.base === 'wstr') {
|
|
167
|
+
const s = String(v ?? '');
|
|
168
|
+
const arr = new Uint16Array(dv.buffer, dv.byteOffset + off, f.count);
|
|
169
|
+
for (let i = 0; i < arr.length; i++)
|
|
170
|
+
arr[i] = 0;
|
|
171
|
+
const n = Math.min(s.length, f.count - 1);
|
|
172
|
+
for (let i = 0; i < n; i++)
|
|
173
|
+
arr[i] = s.charCodeAt(i);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const step = AS[f.base][1];
|
|
177
|
+
const list = (v ?? []);
|
|
178
|
+
for (let i = 0; i < f.count; i++)
|
|
179
|
+
writeScalar(dv, off + i * step, f.base, list[i] ?? 0);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
writeScalar(dv, off, f.base, Number(v));
|
|
183
|
+
}
|
|
184
|
+
function doSize(def) {
|
|
185
|
+
return buildLayout(def).size;
|
|
186
|
+
}
|
|
187
|
+
function readInto(child, buf, offset) {
|
|
188
|
+
const r = child;
|
|
189
|
+
return r.read(buf, offset);
|
|
190
|
+
}
|
|
191
|
+
function doRead(_def, fields, buf, offset) {
|
|
192
|
+
const out = {};
|
|
193
|
+
const dv = new DataView(buf);
|
|
194
|
+
for (const f of fields)
|
|
195
|
+
out[f.name] = readField(dv, offset, f);
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
function doWrite(def, fields, v, buf, offset = 0) {
|
|
199
|
+
buf = buf ?? new ArrayBuffer(doSize(def));
|
|
200
|
+
const dv = new DataView(buf);
|
|
201
|
+
for (const f of fields)
|
|
202
|
+
writeField(dv, offset, f, v[f.name]);
|
|
203
|
+
return buf;
|
|
204
|
+
}
|
|
205
|
+
function writeInto(child, v, buf, offset) {
|
|
206
|
+
const w = child;
|
|
207
|
+
w.write(v, buf, offset);
|
|
208
|
+
}
|
|
209
|
+
export function struct(def) {
|
|
210
|
+
const { fields, size, structAlign } = buildLayout(def);
|
|
211
|
+
return {
|
|
212
|
+
__fields: def,
|
|
213
|
+
size,
|
|
214
|
+
structAlign,
|
|
215
|
+
read: (buf, offset = 0) => doRead(def, fields, buf, offset),
|
|
216
|
+
write: (v, buf, offset = 0) => doWrite(def, fields, v, buf, offset),
|
|
217
|
+
offsetOf: (name) => {
|
|
218
|
+
for (const f of fields)
|
|
219
|
+
if (f.name === name)
|
|
220
|
+
return f.offset;
|
|
221
|
+
throw new Error(`ffi-struct: no field "${String(name)}"`);
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|