win32ole-ffi 0.1.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.
- checksums.yaml +7 -0
- data/BSDL +22 -0
- data/CHANGELOG.md +10 -0
- data/COPYING +56 -0
- data/README.md +49 -0
- data/lib/win32ole/ffi/array.rb +312 -0
- data/lib/win32ole/ffi/dispatch.rb +125 -0
- data/lib/win32ole/ffi/event.rb +800 -0
- data/lib/win32ole/ffi/fiddle_compat.rb +49 -0
- data/lib/win32ole/ffi/method.rb +237 -0
- data/lib/win32ole/ffi/param.rb +111 -0
- data/lib/win32ole/ffi/record.rb +289 -0
- data/lib/win32ole/ffi/type.rb +337 -0
- data/lib/win32ole/ffi/typeinfo.rb +488 -0
- data/lib/win32ole/ffi/typelib.rb +384 -0
- data/lib/win32ole/ffi/variable.rb +117 -0
- data/lib/win32ole/ffi/variant.rb +351 -0
- data/lib/win32ole/ffi/version.rb +5 -0
- data/lib/win32ole/ffi/win32.rb +907 -0
- data/lib/win32ole/ffi/win32ole.rb +827 -0
- data/lib/win32ole/ffi.rb +48 -0
- data/sig/win32ole/ffi.rbs +308 -0
- metadata +84 -0
|
@@ -0,0 +1,907 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fiddle'
|
|
4
|
+
require 'win32ole/ffi/fiddle_compat'
|
|
5
|
+
|
|
6
|
+
class WIN32OLE
|
|
7
|
+
module Win32
|
|
8
|
+
# CRuby's C fiddle defines STDCALL only where libffi has it (32-bit
|
|
9
|
+
# Windows); on x64 Windows the default convention is the same one.
|
|
10
|
+
# JRuby's FFI backend always defines it.
|
|
11
|
+
STDCALL = if Fiddle::Function.const_defined?(:STDCALL)
|
|
12
|
+
Fiddle::Function::STDCALL
|
|
13
|
+
else
|
|
14
|
+
Fiddle::Function::DEFAULT
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
VOIDP = Fiddle::TYPE_VOIDP
|
|
18
|
+
LONG = Fiddle::TYPE_LONG
|
|
19
|
+
DWORD = -Fiddle::TYPE_INT
|
|
20
|
+
WORD = -Fiddle::TYPE_SHORT
|
|
21
|
+
VOID = Fiddle::TYPE_VOID
|
|
22
|
+
|
|
23
|
+
PTR_SIZE = Fiddle::SIZEOF_VOIDP
|
|
24
|
+
PACK_PTR = PTR_SIZE == 8 ? 'Q' : 'L' # native pointer width, for packing real structs (DISPPARAMS, pointer arrays) — NOT for a VARIANT's 8-byte value slot, which always uses 'Q' regardless of platform (see pack_pointer)
|
|
25
|
+
VARIANT_SIZE = PTR_SIZE == 8 ? 24 : 16
|
|
26
|
+
|
|
27
|
+
VT_EMPTY = 0
|
|
28
|
+
VT_NULL = 1
|
|
29
|
+
VT_I2 = 2
|
|
30
|
+
VT_I4 = 3
|
|
31
|
+
VT_R4 = 4
|
|
32
|
+
VT_R8 = 5
|
|
33
|
+
VT_CY = 6
|
|
34
|
+
VT_DATE = 7
|
|
35
|
+
VT_BSTR = 8
|
|
36
|
+
VT_DISPATCH = 9
|
|
37
|
+
VT_ERROR = 10
|
|
38
|
+
VT_BOOL = 11
|
|
39
|
+
VT_VARIANT = 12
|
|
40
|
+
VT_UNKNOWN = 13
|
|
41
|
+
VT_I1 = 16
|
|
42
|
+
VT_UI1 = 17
|
|
43
|
+
VT_UI2 = 18
|
|
44
|
+
VT_UI4 = 19
|
|
45
|
+
VT_I8 = 20
|
|
46
|
+
VT_UI8 = 21
|
|
47
|
+
VT_INT = 22
|
|
48
|
+
VT_UINT = 23
|
|
49
|
+
VT_RECORD = 36
|
|
50
|
+
VT_TYPEMASK = 0x0FFF
|
|
51
|
+
VT_ARRAY = 0x2000
|
|
52
|
+
VT_BYREF = 0x4000
|
|
53
|
+
|
|
54
|
+
DISPATCH_METHOD = 1
|
|
55
|
+
DISPATCH_PROPERTYGET = 2
|
|
56
|
+
DISPATCH_PROPERTYPUT = 4
|
|
57
|
+
DISPATCH_PROPERTYPUTREF = 8
|
|
58
|
+
DISPID_PROPERTYPUT = -3
|
|
59
|
+
DISPID_VALUE = 0
|
|
60
|
+
|
|
61
|
+
CLSCTX_INPROC_SERVER = 0x1
|
|
62
|
+
CLSCTX_LOCAL_SERVER = 0x4
|
|
63
|
+
CLSCTX_REMOTE_SERVER = 0x10
|
|
64
|
+
|
|
65
|
+
LOCALE_SYSTEM_DEFAULT = 0x0800
|
|
66
|
+
LOCALE_USER_DEFAULT = 0x0400
|
|
67
|
+
LCID_INSTALLED = 0x00000001
|
|
68
|
+
|
|
69
|
+
CP_ACP = 0
|
|
70
|
+
CP_OEMCP = 1
|
|
71
|
+
CP_MACCP = 2
|
|
72
|
+
CP_THREAD_ACP = 3
|
|
73
|
+
CP_SYMBOL = 42
|
|
74
|
+
CP_UTF7 = 65000
|
|
75
|
+
CP_UTF8 = 65001
|
|
76
|
+
CP_EUC_JP_51932 = 51932
|
|
77
|
+
# set_ole_codepage accepts these even when not "installed".
|
|
78
|
+
SPECIAL_CODEPAGES = [CP_ACP, CP_OEMCP, CP_MACCP, CP_THREAD_ACP, CP_SYMBOL, CP_UTF7, CP_UTF8, CP_EUC_JP_51932].freeze
|
|
79
|
+
CODEPAGE_ERROR_MESSAGE = 'codepage should be WIN32OLE::CP_ACP, WIN32OLE::CP_OEMCP, WIN32OLE::CP_MACCP, ' \
|
|
80
|
+
'WIN32OLE::CP_THREAD_ACP, WIN32OLE::CP_SYMBOL, WIN32OLE::CP_UTF7, WIN32OLE::CP_UTF8, ' \
|
|
81
|
+
'or installed codepage.'
|
|
82
|
+
|
|
83
|
+
# ext/win32ole/win32ole.c's ole_encoding2cp table (names compared
|
|
84
|
+
# case-insensitively); anything else maps to CP_ACP.
|
|
85
|
+
ENCODING_CODEPAGES = {
|
|
86
|
+
'big5' => 950, 'cp51932' => 51932, 'cp850' => 850, 'cp852' => 852, 'cp855' => 855, 'cp949' => 949,
|
|
87
|
+
'euc-jp' => 20932, 'euc-kr' => 51949, 'euc-tw' => 51950, 'gb18030' => 54936, 'gb2312' => 20936,
|
|
88
|
+
'gbk' => 936, 'ibm437' => 437, 'ibm720' => 720, 'ibm737' => 737, 'ibm775' => 775, 'ibm852' => 852,
|
|
89
|
+
'ibm855' => 855, 'ibm857' => 857, 'ibm860' => 860, 'ibm861' => 861, 'ibm862' => 862, 'ibm863' => 863,
|
|
90
|
+
'ibm864' => 864, 'ibm865' => 865, 'ibm866' => 866, 'ibm869' => 869, 'iso-2022-jp' => 50220,
|
|
91
|
+
'iso-8859-1' => 28591, 'iso-8859-15' => 28605, 'iso-8859-2' => 28592, 'iso-8859-3' => 28593,
|
|
92
|
+
'iso-8859-4' => 28594, 'iso-8859-5' => 28595, 'iso-8859-6' => 28596, 'iso-8859-7' => 28597,
|
|
93
|
+
'iso-8859-8' => 28598, 'iso-8859-9' => 28599, 'koi8-r' => 20866, 'koi8-u' => 21866, 'shift_jis' => 932,
|
|
94
|
+
'utf-16be' => 1201, 'utf-16le' => 1200, 'utf-7' => 65000, 'utf-8' => 65001, 'windows-1250' => 1250,
|
|
95
|
+
'windows-1251' => 1251, 'windows-1252' => 1252, 'windows-1253' => 1253, 'windows-1254' => 1254,
|
|
96
|
+
'windows-1255' => 1255, 'windows-1256' => 1256, 'windows-1257' => 1257, 'windows-1258' => 1258,
|
|
97
|
+
'windows-31j' => 932, 'windows-874' => 874, 'eucjp-ms' => 20932
|
|
98
|
+
}.freeze
|
|
99
|
+
|
|
100
|
+
CPINFOEX_SIZE = 4 + 2 + 12 + 2 + 4 + (260 * 2) # MaxCharSize..CodePage + WCHAR CodePageName[MAX_PATH]
|
|
101
|
+
CPINFOEX_CODEPAGE_OFFSET = 20
|
|
102
|
+
|
|
103
|
+
IID_NULL = ("\x00" * 16).b
|
|
104
|
+
IID_IDISPATCH = [0x00020400, 0, 0, 0xC0, 0, 0, 0, 0, 0, 0, 0x46].pack('LSSC8')
|
|
105
|
+
IID_IENUMVARIANT = [0x00020404, 0, 0, 0xC0, 0, 0, 0, 0, 0, 0, 0x46].pack('LSSC8')
|
|
106
|
+
IID_IPERSISTMEMORY = [0xBD1AE5E0, 0xA6AE, 0x11CE, 0xBD, 0x37, 0x50, 0x42, 0x00, 0xC1, 0x00, 0x00].pack('LSSC8')
|
|
107
|
+
DISPID_NEWENUM = -4
|
|
108
|
+
|
|
109
|
+
# Well-known, decades-stable OLE Automation interface IDs (ocidl.h /
|
|
110
|
+
# objbase.h). Hand-transcribed, not pulled from a live header -- verify
|
|
111
|
+
# against a real Windows build before anything else depends on it.
|
|
112
|
+
IID_IUNKNOWN = [0, 0, 0, 0xC0, 0, 0, 0, 0, 0, 0, 0x46].pack('LSSC8')
|
|
113
|
+
IID_ICONNECTIONPOINTCONTAINER = [0xB196B284, 0xBAB4, 0x101A, 0xB6, 0x9C, 0x00, 0xAA, 0x00, 0x34, 0x1D, 0x07].pack('LSSC8')
|
|
114
|
+
IID_IPROVIDECLASSINFO = [0xB196B283, 0xBAB4, 0x101A, 0xB6, 0x9C, 0x00, 0xAA, 0x00, 0x34, 0x1D, 0x07].pack('LSSC8')
|
|
115
|
+
IID_IPROVIDECLASSINFO2 = [0xA6BC3AC0, 0xDBAA, 0x11CE, 0x9D, 0xE3, 0x00, 0xAA, 0x00, 0x4B, 0xB8, 0x51].pack('LSSC8')
|
|
116
|
+
|
|
117
|
+
VT_FOR_TYPE = {
|
|
118
|
+
i4: VT_I4, i8: VT_I8, r8: VT_R8, bool: VT_BOOL,
|
|
119
|
+
empty: VT_EMPTY, bstr: VT_BSTR, dispatch: VT_DISPATCH, date: VT_DATE
|
|
120
|
+
}.freeze
|
|
121
|
+
|
|
122
|
+
INT32_RANGE = (-(2**31))..(2**31 - 1)
|
|
123
|
+
|
|
124
|
+
# EXCEPINFO (oaidl.h): WORD wCode; WORD wReserved; BSTR bstrSource;
|
|
125
|
+
# BSTR bstrDescription; BSTR bstrHelpFile; DWORD dwHelpContext;
|
|
126
|
+
# PVOID pvReserved; HRESULT(*pfnDeferredFillIn)(...); SCODE scode;
|
|
127
|
+
if PTR_SIZE == 8
|
|
128
|
+
EXCEPINFO_SIZE = 64
|
|
129
|
+
EXCEPINFO_OFFSETS = {
|
|
130
|
+
wCode: 0, bstrSource: 8, bstrDescription: 16, bstrHelpFile: 24,
|
|
131
|
+
dwHelpContext: 32, pvReserved: 40, pfnDeferredFillIn: 48, scode: 56
|
|
132
|
+
}.freeze
|
|
133
|
+
else
|
|
134
|
+
EXCEPINFO_SIZE = 32
|
|
135
|
+
EXCEPINFO_OFFSETS = {
|
|
136
|
+
wCode: 0, bstrSource: 4, bstrDescription: 8, bstrHelpFile: 12,
|
|
137
|
+
dwHelpContext: 16, pvReserved: 20, pfnDeferredFillIn: 24, scode: 28
|
|
138
|
+
}.freeze
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
module_function
|
|
142
|
+
|
|
143
|
+
# Ruby -> NUL-terminated UTF-16LE. Like ole_vstr2wc, the string's own
|
|
144
|
+
# encoding decides the conversion; an encoding Ruby can't transcode
|
|
145
|
+
# (a dummy one such as "UTF-16", or binary with non-ASCII bytes) falls
|
|
146
|
+
# back to the ANSI code page, which is what ole_encoding2cp's CP_ACP
|
|
147
|
+
# default does for encodings outside its table.
|
|
148
|
+
def wstr(str)
|
|
149
|
+
s = str.is_a?(::String) ? str : str.to_s
|
|
150
|
+
ansi = s.encoding.dummy? || (s.encoding == Encoding::BINARY && !s.ascii_only?)
|
|
151
|
+
wide = ansi ? multi_byte_to_wide_char(CP_ACP, s) : s.encode('UTF-16LE').b
|
|
152
|
+
wide << "\x00\x00".b
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def encoding_to_codepage(enc)
|
|
156
|
+
ENCODING_CODEPAGES.fetch(enc.name.downcase, CP_ACP)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def codepage
|
|
160
|
+
init_codepage unless @codepage
|
|
161
|
+
@codepage
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# set_ole_codepage. On top of C's checks, the codepage must map to a
|
|
165
|
+
# Ruby Encoding ("CP<n>", dummy ones included): C defines a dummy
|
|
166
|
+
# encoding for any that doesn't, which isn't possible from Ruby, and
|
|
167
|
+
# strings returned from COM have to be tagged with something.
|
|
168
|
+
def codepage=(cp)
|
|
169
|
+
raise TypeError, "no implicit conversion of #{cp.class} into Integer" unless cp.is_a?(::Integer)
|
|
170
|
+
unless SPECIAL_CODEPAGES.include?(cp) || is_valid_code_page.call(cp) != 0
|
|
171
|
+
raise WIN32OLE::RuntimeError, CODEPAGE_ERROR_MESSAGE
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
real = real_codepage(cp)
|
|
175
|
+
enc = begin
|
|
176
|
+
Encoding.find("CP#{real}")
|
|
177
|
+
rescue ArgumentError
|
|
178
|
+
raise WIN32OLE::RuntimeError, "#{CODEPAGE_ERROR_MESSAGE} (no Ruby encoding for CP#{real})"
|
|
179
|
+
end
|
|
180
|
+
@codepage = cp
|
|
181
|
+
@codepage_encoding = enc
|
|
182
|
+
@real_codepage = real
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def init_codepage
|
|
186
|
+
self.codepage = encoding_to_codepage(Encoding.default_internal || Encoding.default_external)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# ole_cp2encoding's mapping of the pseudo codepages to a real one.
|
|
190
|
+
def real_codepage(cp)
|
|
191
|
+
case cp
|
|
192
|
+
when CP_ACP then get_acp.call
|
|
193
|
+
when CP_OEMCP then get_oemcp.call
|
|
194
|
+
when CP_MACCP, CP_THREAD_ACP
|
|
195
|
+
info = ("\x00" * CPINFOEX_SIZE).b
|
|
196
|
+
raise WIN32OLE::RuntimeError, 'cannot map codepage to encoding.' if get_cp_info_ex.call(cp, 0, info).zero?
|
|
197
|
+
|
|
198
|
+
info[CPINFOEX_CODEPAGE_OFFSET, 4].unpack1('L')
|
|
199
|
+
else cp
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# UTF-16LE bytes -> a String in WIN32OLE.codepage, the way ole_wc2vstr
|
|
204
|
+
# tags COM strings with cWIN32OLE_enc. Unconvertible characters become
|
|
205
|
+
# '?', as WideCharToMultiByte's default character does.
|
|
206
|
+
def utf16_to_s(utf16)
|
|
207
|
+
init_codepage unless @codepage
|
|
208
|
+
if @codepage_encoding.dummy?
|
|
209
|
+
wide_char_to_multi_byte(@real_codepage, utf16).force_encoding(@codepage_encoding)
|
|
210
|
+
else
|
|
211
|
+
utf16.dup.force_encoding('UTF-16LE').encode(@codepage_encoding, invalid: :replace, undef: :replace)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def pack_variant(vt, body)
|
|
216
|
+
body = body.b
|
|
217
|
+
max = VARIANT_SIZE - 8
|
|
218
|
+
if body.bytesize > max
|
|
219
|
+
raise ArgumentError, "body must be <= #{max} bytes, got #{body.bytesize}"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
[vt, 0, 0, 0].pack('S4') + body + ("\x00".b * (max - body.bytesize))
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def unpack_variant(bytes, body_size: 8)
|
|
226
|
+
vt, = bytes.unpack1('S')
|
|
227
|
+
[vt, bytes[8, body_size]]
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def pack_i1(value); [value].pack('c') + ("\x00".b * 7); end
|
|
231
|
+
def pack_ui1(value); [value].pack('C') + ("\x00".b * 7); end
|
|
232
|
+
def pack_i2(value); [value].pack('s') + ("\x00".b * 6); end
|
|
233
|
+
def pack_ui2(value); [value].pack('S') + ("\x00".b * 6); end
|
|
234
|
+
def pack_i4(value); [value].pack('l') + ("\x00".b * 4); end
|
|
235
|
+
def pack_ui4(value); [value].pack('L') + ("\x00".b * 4); end
|
|
236
|
+
def pack_i8(value); [value].pack('q'); end
|
|
237
|
+
def pack_ui8(value); [value].pack('Q'); end
|
|
238
|
+
def pack_int(value); pack_i4(value); end
|
|
239
|
+
def pack_uint(value); pack_ui4(value); end
|
|
240
|
+
def pack_r4(value); [value].pack('f') + ("\x00".b * 4); end
|
|
241
|
+
def pack_r8(value); [value].pack('d'); end
|
|
242
|
+
def pack_bool(value); [value ? -1 : 0].pack('s') + ("\x00".b * 6); end
|
|
243
|
+
def pack_pointer(addr); [addr].pack('Q'); end
|
|
244
|
+
def pack_empty; "\x00".b * 8; end
|
|
245
|
+
def pack_error(value); pack_i4(value); end
|
|
246
|
+
|
|
247
|
+
def unpack_i1(payload); payload.unpack1('c'); end
|
|
248
|
+
def unpack_ui1(payload); payload.unpack1('C'); end
|
|
249
|
+
def unpack_i2(payload); payload.unpack1('s'); end
|
|
250
|
+
def unpack_ui2(payload); payload.unpack1('S'); end
|
|
251
|
+
def unpack_i4(payload); payload.unpack1('l'); end
|
|
252
|
+
def unpack_ui4(payload); payload.unpack1('L'); end
|
|
253
|
+
def unpack_i8(payload); payload.unpack1('q'); end
|
|
254
|
+
def unpack_ui8(payload); payload.unpack1('Q'); end
|
|
255
|
+
def unpack_int(payload); unpack_i4(payload); end
|
|
256
|
+
def unpack_uint(payload); unpack_ui4(payload); end
|
|
257
|
+
def unpack_r4(payload); payload.unpack1('f'); end
|
|
258
|
+
def unpack_r8(payload); payload.unpack1('d'); end
|
|
259
|
+
def unpack_bool(payload); payload.unpack1('s') != 0; end
|
|
260
|
+
def unpack_pointer(payload); payload.unpack1('Q'); end
|
|
261
|
+
def unpack_error(payload); unpack_i4(payload); end
|
|
262
|
+
|
|
263
|
+
def ruby_to_variant_type(value)
|
|
264
|
+
case value
|
|
265
|
+
when String then :bstr
|
|
266
|
+
when Integer then INT32_RANGE.cover?(value) ? :i4 : :i8
|
|
267
|
+
when Float then :r8
|
|
268
|
+
when true, false then :bool
|
|
269
|
+
when nil then :empty
|
|
270
|
+
when ::WIN32OLE then :dispatch
|
|
271
|
+
when ::Time then :date
|
|
272
|
+
else
|
|
273
|
+
raise TypeError, "unsupported argument type for OLE call: #{value.class}"
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def variant_ruby_type(vt)
|
|
278
|
+
case vt
|
|
279
|
+
# A bare VT_VARIANT holds no value: ole_variant2val's VT_BSTR
|
|
280
|
+
# conversion of it always fails, giving nil.
|
|
281
|
+
when VT_EMPTY, VT_NULL, VT_VARIANT then :empty
|
|
282
|
+
when VT_DATE then :date
|
|
283
|
+
when VT_I4 then :i4
|
|
284
|
+
when VT_I8 then :i8
|
|
285
|
+
when VT_R8 then :r8
|
|
286
|
+
when VT_BOOL then :bool
|
|
287
|
+
when VT_BSTR then :bstr
|
|
288
|
+
when VT_DISPATCH, VT_UNKNOWN then :dispatch
|
|
289
|
+
when VT_I1 then :i1
|
|
290
|
+
when VT_UI1 then :ui1
|
|
291
|
+
when VT_I2 then :i2
|
|
292
|
+
when VT_UI2 then :ui2
|
|
293
|
+
when VT_UI4 then :ui4
|
|
294
|
+
when VT_UI8 then :ui8
|
|
295
|
+
when VT_INT then :int
|
|
296
|
+
when VT_UINT then :uint
|
|
297
|
+
when VT_R4 then :r4
|
|
298
|
+
when VT_ERROR then :error
|
|
299
|
+
else :other # VT_CY, VT_DECIMAL, ...: ole_variant2val's VT_BSTR fallback
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Bytes a VT_BYREF pointer refers to, per referenced type.
|
|
304
|
+
VALUE_SIZES = {
|
|
305
|
+
VT_I1 => 1, VT_UI1 => 1, VT_I2 => 2, VT_UI2 => 2, VT_BOOL => 2,
|
|
306
|
+
VT_I4 => 4, VT_UI4 => 4, VT_INT => 4, VT_UINT => 4, VT_R4 => 4, VT_ERROR => 4
|
|
307
|
+
}.freeze
|
|
308
|
+
|
|
309
|
+
def value_size(vt)
|
|
310
|
+
return PTR_SIZE if (vt & VT_ARRAY) != 0 || [VT_BSTR, VT_DISPATCH, VT_UNKNOWN].include?(vt)
|
|
311
|
+
|
|
312
|
+
VALUE_SIZES.fetch(vt, 8)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# The non-BYREF VARIANT a VT_BYREF one refers to (a copy of its bytes),
|
|
316
|
+
# following VT_VARIANT|VT_BYREF chains as ole_variant2val does; a
|
|
317
|
+
# non-BYREF VARIANT is returned as is.
|
|
318
|
+
def deref_variant(bytes)
|
|
319
|
+
vt, payload = unpack_variant(bytes)
|
|
320
|
+
while (vt & VT_BYREF) != 0
|
|
321
|
+
base_vt = vt & ~VT_BYREF
|
|
322
|
+
addr = unpack_pointer(payload)
|
|
323
|
+
return pack_variant(base_vt, pack_empty) if addr.zero?
|
|
324
|
+
|
|
325
|
+
if base_vt == VT_VARIANT
|
|
326
|
+
bytes = Fiddle::Pointer.new(addr)[0, VARIANT_SIZE]
|
|
327
|
+
vt, payload = unpack_variant(bytes)
|
|
328
|
+
else
|
|
329
|
+
data = Fiddle::Pointer.new(addr)[0, value_size(base_vt)]
|
|
330
|
+
return pack_variant(base_vt, data.ljust(8, "\x00".b))
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
bytes
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# VariantChangeTypeEx under WIN32OLE.locale; returns the new VARIANT.
|
|
337
|
+
def change_variant_type(variant_bytes, vt)
|
|
338
|
+
dest = ("\x00" * VARIANT_SIZE).b
|
|
339
|
+
hr = variant_change_type.call(dest, variant_bytes, WIN32OLE.locale, 0, vt)
|
|
340
|
+
raise WIN32OLE::RuntimeError, "failed to change variant type: #{hr_hex(hr)}" if failed?(hr)
|
|
341
|
+
|
|
342
|
+
dest
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def add_ref(iunknown_ptr)
|
|
346
|
+
vtable_function(iunknown_ptr, 1, [VOIDP], DWORD).call(iunknown_ptr)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
OLE_DATE_EPOCH = Time.utc(1899, 12, 30)
|
|
350
|
+
SECONDS_PER_DAY = 86_400
|
|
351
|
+
|
|
352
|
+
# An OLE DATE is days since 1899-12-30, but for a negative DATE the
|
|
353
|
+
# fraction is the time of day itself (-1.25 is 1899-12-29 06:00), not
|
|
354
|
+
# an offset toward zero. Like vtdate2rbtime/rbtime2vtdate, these work
|
|
355
|
+
# on wall-clock fields in the local zone; computing them in UTC avoids
|
|
356
|
+
# VariantTimeToSystemTime's whole-second rounding that C has to undo.
|
|
357
|
+
def vtdate_to_time(date)
|
|
358
|
+
days = date.truncate
|
|
359
|
+
seconds = (days * SECONDS_PER_DAY) + ((date - days).abs * SECONDS_PER_DAY)
|
|
360
|
+
wall = OLE_DATE_EPOCH + seconds
|
|
361
|
+
Time.new(wall.year, wall.month, wall.day, wall.hour, wall.min, wall.sec + wall.subsec)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def time_to_vtdate(time)
|
|
365
|
+
wall = Time.utc(time.year, time.month, time.day, time.hour, time.min, time.sec + time.subsec)
|
|
366
|
+
days = (wall - OLE_DATE_EPOCH) / SECONDS_PER_DAY
|
|
367
|
+
whole = days.floor
|
|
368
|
+
whole.negative? && days != whole ? whole - (days - whole) : days.to_f
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def dispatch_plan(name, args)
|
|
372
|
+
if name.end_with?('=')
|
|
373
|
+
unless args.size == 1
|
|
374
|
+
raise ArgumentError, "property put takes exactly one argument, got #{args.size}"
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# ole_propertyput sets both: PROPERTYPUTREF lets a by-reference
|
|
378
|
+
# (object-valued) property be assigned as well.
|
|
379
|
+
{ name: name[0..-2], wflags: DISPATCH_PROPERTYPUT | DISPATCH_PROPERTYPUTREF, named_put: true }
|
|
380
|
+
else
|
|
381
|
+
{ name: name, wflags: DISPATCH_METHOD | DISPATCH_PROPERTYGET, named_put: false }
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def failed?(hr)
|
|
386
|
+
hr.negative?
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# ole_initialized(): whether this thread has called OleInitialize, as
|
|
390
|
+
# WIN32OLE.ole_initialize records it.
|
|
391
|
+
def ole_initialized?
|
|
392
|
+
Thread.current.thread_variable_get(:win32ole_ole_initialized)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# Releases a finalizer couldn't do on its thread. CRuby usually runs
|
|
396
|
+
# finalizers on the main thread, which has OLE, so this is rare there.
|
|
397
|
+
# JRuby runs every finalizer on a "Finalizer" thread of its own, which
|
|
398
|
+
# never initializes OLE; skipping the Release there, as the C extension
|
|
399
|
+
# does, would leak every COM object GC collects. So the Release waits
|
|
400
|
+
# here instead.
|
|
401
|
+
#
|
|
402
|
+
# A Queue, not an Array with a Mutex: a finalizer may run in the middle
|
|
403
|
+
# of run_deferred_releases on the same thread (CRuby runs finalizers at
|
|
404
|
+
# safe points, e.g. an allocation), and re-locking a Mutex this thread
|
|
405
|
+
# already holds would raise. Queue#<< needs no lock of ours.
|
|
406
|
+
DEFERRED_RELEASES = Queue.new
|
|
407
|
+
|
|
408
|
+
# OLE_FREE: Release now if this thread has initialized OLE, otherwise
|
|
409
|
+
# on the next thread with OLE that calls run_deferred_releases. Like
|
|
410
|
+
# OLE_FREE, any thread with OLE will do, not only the object's own
|
|
411
|
+
# apartment.
|
|
412
|
+
def release_on_ole_thread(&release)
|
|
413
|
+
ole_initialized? ? release.call : DEFERRED_RELEASES << release
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# WIN32OLE.ole_initialize calls this, so the queue empties whenever a
|
|
417
|
+
# thread with OLE next creates or connects to an object. Releases still
|
|
418
|
+
# queued at exit never run, as they wouldn't in the C extension.
|
|
419
|
+
def run_deferred_releases
|
|
420
|
+
loop do
|
|
421
|
+
release = begin
|
|
422
|
+
DEFERRED_RELEASES.pop(true)
|
|
423
|
+
rescue ThreadError # empty
|
|
424
|
+
break
|
|
425
|
+
end
|
|
426
|
+
release.call
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def hr_hex(hr)
|
|
431
|
+
format('0x%08x', hr & 0xFFFFFFFF)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def method_error_message(method_name, detail)
|
|
435
|
+
"(in OLE method `#{method_name}': )#{detail}"
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def property_put_error_message(property_name, detail)
|
|
439
|
+
"(in setting property `#{property_name}': )#{detail}"
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def query_interface_error_message(operation, detail)
|
|
443
|
+
"failed to #{operation}: #{detail}"
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def unknown_server_error_message(server_name)
|
|
447
|
+
"unknown OLE server: `#{server_name}'"
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def parse_excepinfo(bytes)
|
|
451
|
+
o = EXCEPINFO_OFFSETS
|
|
452
|
+
ptr_fmt = PTR_SIZE == 8 ? 'Q' : 'L'
|
|
453
|
+
{
|
|
454
|
+
w_code: bytes[o[:wCode], 2].unpack1('S'),
|
|
455
|
+
bstr_source_ptr: bytes[o[:bstrSource], PTR_SIZE].unpack1(ptr_fmt),
|
|
456
|
+
bstr_description_ptr: bytes[o[:bstrDescription], PTR_SIZE].unpack1(ptr_fmt),
|
|
457
|
+
scode: bytes[o[:scode], 4].unpack1('l')
|
|
458
|
+
}
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def ole32
|
|
462
|
+
@ole32 ||= Fiddle.dlopen('ole32')
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def oleaut32
|
|
466
|
+
@oleaut32 ||= Fiddle.dlopen('oleaut32')
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def kernel32
|
|
470
|
+
@kernel32 ||= Fiddle.dlopen('kernel32')
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def is_valid_code_page
|
|
474
|
+
@is_valid_code_page ||= Fiddle::Function.new(kernel32['IsValidCodePage'], [DWORD], DWORD, STDCALL)
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def get_acp
|
|
478
|
+
@get_acp ||= Fiddle::Function.new(kernel32['GetACP'], [], DWORD, STDCALL)
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def get_oemcp
|
|
482
|
+
@get_oemcp ||= Fiddle::Function.new(kernel32['GetOEMCP'], [], DWORD, STDCALL)
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def get_cp_info_ex
|
|
486
|
+
@get_cp_info_ex ||= Fiddle::Function.new(kernel32['GetCPInfoExW'], [DWORD, DWORD, VOIDP], DWORD, STDCALL)
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def multi_byte_to_wide_char_fn
|
|
490
|
+
@multi_byte_to_wide_char_fn ||= Fiddle::Function.new(
|
|
491
|
+
kernel32['MultiByteToWideChar'], [DWORD, DWORD, VOIDP, LONG, VOIDP, LONG], LONG, STDCALL
|
|
492
|
+
)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def wide_char_to_multi_byte_fn
|
|
496
|
+
@wide_char_to_multi_byte_fn ||= Fiddle::Function.new(
|
|
497
|
+
kernel32['WideCharToMultiByte'], [DWORD, DWORD, VOIDP, LONG, VOIDP, LONG, VOIDP, VOIDP], LONG, STDCALL
|
|
498
|
+
)
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# Returns UTF-16LE bytes (no terminator).
|
|
502
|
+
def multi_byte_to_wide_char(cp, str)
|
|
503
|
+
bytes = str.b
|
|
504
|
+
return ''.b if bytes.empty?
|
|
505
|
+
|
|
506
|
+
count = multi_byte_to_wide_char_fn.call(cp, 0, bytes, bytes.bytesize, nil, 0)
|
|
507
|
+
raise WIN32OLE::RuntimeError, "fail to convert CP#{cp} to Unicode" if count.zero?
|
|
508
|
+
|
|
509
|
+
out = ("\x00" * (count * 2)).b
|
|
510
|
+
multi_byte_to_wide_char_fn.call(cp, 0, bytes, bytes.bytesize, out, count)
|
|
511
|
+
out
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# UTF-16LE bytes in, codepage bytes (binary String) out.
|
|
515
|
+
def wide_char_to_multi_byte(cp, utf16)
|
|
516
|
+
chars = utf16.bytesize / 2
|
|
517
|
+
return ''.b if chars.zero?
|
|
518
|
+
|
|
519
|
+
size = wide_char_to_multi_byte_fn.call(cp, 0, utf16, chars, nil, 0, nil, nil)
|
|
520
|
+
raise WIN32OLE::RuntimeError, "fail to convert Unicode to CP#{cp}" if size.zero?
|
|
521
|
+
|
|
522
|
+
out = ("\x00" * size).b
|
|
523
|
+
wide_char_to_multi_byte_fn.call(cp, 0, utf16, chars, out, size, nil, nil)
|
|
524
|
+
out
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def expand_environment_strings_fn
|
|
528
|
+
@expand_environment_strings_fn ||= Fiddle::Function.new(
|
|
529
|
+
kernel32['ExpandEnvironmentStringsW'], [VOIDP, VOIDP, DWORD], DWORD, STDCALL
|
|
530
|
+
)
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
# Registry values like a COM class's InprocServer32 path may contain
|
|
534
|
+
# %ENVVAR%-style references (e.g. "%SystemRoot%\\system32\\shell32.dll")
|
|
535
|
+
# that ExpandEnvironmentStrings resolves -- mirrors
|
|
536
|
+
# ext/win32ole/win32ole_typelib.c's typelib_file_from_clsid.
|
|
537
|
+
def expand_environment_strings(str)
|
|
538
|
+
return str unless str.include?('%')
|
|
539
|
+
|
|
540
|
+
out = ("\x00" * 520).b # 260 WCHARs, generous for a MAX_PATH-style value
|
|
541
|
+
len = expand_environment_strings_fn.call(wstr(str), out, 260)
|
|
542
|
+
return str if len.zero? || len > 260
|
|
543
|
+
|
|
544
|
+
out[0, (len - 1) * 2].force_encoding('UTF-16LE').encode('UTF-8')
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def ole_initialize_fn
|
|
548
|
+
@ole_initialize_fn ||= Fiddle::Function.new(ole32['OleInitialize'], [VOIDP], LONG, STDCALL)
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def ole_uninitialize_fn
|
|
552
|
+
@ole_uninitialize_fn ||= Fiddle::Function.new(ole32['OleUninitialize'], [], VOID, STDCALL)
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def clsid_from_progid
|
|
556
|
+
@clsid_from_progid ||= Fiddle::Function.new(ole32['CLSIDFromProgID'], [VOIDP, VOIDP], LONG, STDCALL)
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def clsid_from_string
|
|
560
|
+
@clsid_from_string ||= Fiddle::Function.new(ole32['CLSIDFromString'], [VOIDP, VOIDP], LONG, STDCALL)
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
def co_create_instance
|
|
564
|
+
@co_create_instance ||= Fiddle::Function.new(
|
|
565
|
+
ole32['CoCreateInstance'], [VOIDP, VOIDP, DWORD, VOIDP, VOIDP], LONG, STDCALL
|
|
566
|
+
)
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def co_create_instance_ex
|
|
570
|
+
@co_create_instance_ex ||= Fiddle::Function.new(
|
|
571
|
+
ole32['CoCreateInstanceEx'], [VOIDP, VOIDP, DWORD, VOIDP, DWORD, VOIDP], LONG, STDCALL
|
|
572
|
+
)
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
HKEY_LOCAL_MACHINE = 0x80000002
|
|
576
|
+
|
|
577
|
+
def reg_connect_registry
|
|
578
|
+
@reg_connect_registry ||= Fiddle::Function.new(
|
|
579
|
+
TypeInfo.advapi32['RegConnectRegistryW'], [VOIDP, VOIDP, VOIDP], LONG, STDCALL
|
|
580
|
+
)
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
# clsid_from_remote: +progid+'s CLSID as registered on +host+
|
|
584
|
+
# (HKLM\SOFTWARE\Classes\<progid>\CLSID there), or nil.
|
|
585
|
+
def clsid_from_remote(host, progid)
|
|
586
|
+
out = ("\x00" * PTR_SIZE).b
|
|
587
|
+
return nil unless reg_connect_registry.call(wstr(host), HKEY_LOCAL_MACHINE, out).zero?
|
|
588
|
+
|
|
589
|
+
hlm = out.unpack1(PACK_PTR)
|
|
590
|
+
begin
|
|
591
|
+
value = TypeLib.with_reg_subkey(hlm, "SOFTWARE\\Classes\\#{progid}\\CLSID") do |hkey|
|
|
592
|
+
TypeLib.reg_default_string_value(hkey)
|
|
593
|
+
end
|
|
594
|
+
return nil unless value
|
|
595
|
+
|
|
596
|
+
clsid = ("\x00" * 16).b
|
|
597
|
+
failed?(clsid_from_string.call(wstr(value), clsid)) ? nil : clsid
|
|
598
|
+
ensure
|
|
599
|
+
TypeInfo.reg_close_key.call(hlm)
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def create_bind_ctx
|
|
604
|
+
@create_bind_ctx ||= Fiddle::Function.new(ole32['CreateBindCtx'], [DWORD, VOIDP], LONG, STDCALL)
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
def mk_parse_display_name
|
|
608
|
+
@mk_parse_display_name ||= Fiddle::Function.new(
|
|
609
|
+
ole32['MkParseDisplayName'], [VOIDP, VOIDP, VOIDP, VOIDP], LONG, STDCALL
|
|
610
|
+
)
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def get_active_object
|
|
614
|
+
@get_active_object ||= Fiddle::Function.new(oleaut32['GetActiveObject'], [VOIDP, VOIDP, VOIDP], LONG, STDCALL)
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
def co_task_mem_free
|
|
618
|
+
@co_task_mem_free ||= Fiddle::Function.new(ole32['CoTaskMemFree'], [VOIDP], VOID, STDCALL)
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
def co_create_guid
|
|
622
|
+
@co_create_guid ||= Fiddle::Function.new(ole32['CoCreateGuid'], [VOIDP], LONG, STDCALL)
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
def prog_id_from_clsid_fn
|
|
626
|
+
# ProgIDFromCLSID is an ole32 export, not oleaut32.
|
|
627
|
+
@prog_id_from_clsid_fn ||= Fiddle::Function.new(ole32['ProgIDFromCLSID'], [VOIDP, VOIDP], LONG, STDCALL)
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
def prog_id_from_clsid(clsid_bytes)
|
|
631
|
+
out = ("\x00" * PTR_SIZE).b
|
|
632
|
+
hr = prog_id_from_clsid_fn.call(clsid_bytes, out)
|
|
633
|
+
return nil if failed?(hr)
|
|
634
|
+
|
|
635
|
+
# ProgIDFromCLSID's output is a CoTaskMemAlloc'd, NUL-terminated wide
|
|
636
|
+
# string -- NOT a BSTR. Reading it with bstr_to_s (which trusts a
|
|
637
|
+
# length prefix 4 bytes before the string) would read garbage-length
|
|
638
|
+
# data out of bounds, and freeing it with SysFreeString (the BSTR
|
|
639
|
+
# deallocator) would free CoTaskMem-allocated memory with the wrong
|
|
640
|
+
# allocator, corrupting the heap.
|
|
641
|
+
addr = out.unpack1(PACK_PTR)
|
|
642
|
+
str = wstr_to_s(addr)
|
|
643
|
+
co_task_mem_free.call(addr) unless addr.zero?
|
|
644
|
+
str
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
def sys_alloc_string
|
|
648
|
+
@sys_alloc_string ||= Fiddle::Function.new(oleaut32['SysAllocString'], [VOIDP], VOIDP, STDCALL)
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
def sys_free_string
|
|
652
|
+
@sys_free_string ||= Fiddle::Function.new(oleaut32['SysFreeString'], [VOIDP], VOID, STDCALL)
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
def enum_system_locales_a
|
|
656
|
+
@enum_system_locales_a ||= Fiddle::Function.new(
|
|
657
|
+
kernel32['EnumSystemLocalesA'], [VOIDP, DWORD], DWORD, STDCALL
|
|
658
|
+
)
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
# Mirrors ext/win32ole/win32ole.c's lcid_installed: EnumSystemLocalesA
|
|
662
|
+
# hands each installed LCID to the callback as an 8-hex-digit ANSI
|
|
663
|
+
# string (e.g. "00000409"), lowercase, zero-padded -- same format
|
|
664
|
+
# produced by C's "%08lx". Returning 0 (FALSE) from the callback stops
|
|
665
|
+
# the enumeration early once a match is found.
|
|
666
|
+
def locale_installed?(lcid)
|
|
667
|
+
target = format('%08x', lcid)
|
|
668
|
+
installed = false
|
|
669
|
+
callback = Fiddle::Closure::BlockCaller.new(DWORD, [VOIDP], STDCALL) do |str_ptr|
|
|
670
|
+
if Fiddle::Pointer.new(str_ptr)[0, 8] == target
|
|
671
|
+
installed = true
|
|
672
|
+
0
|
|
673
|
+
else
|
|
674
|
+
1
|
|
675
|
+
end
|
|
676
|
+
end
|
|
677
|
+
enum_system_locales_a.call(callback, LCID_INSTALLED)
|
|
678
|
+
installed
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100
|
|
682
|
+
FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
|
|
683
|
+
FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200
|
|
684
|
+
|
|
685
|
+
def format_message
|
|
686
|
+
@format_message ||= Fiddle::Function.new(
|
|
687
|
+
kernel32['FormatMessageW'], [DWORD, VOIDP, DWORD, DWORD, VOIDP, DWORD, VOIDP], DWORD, STDCALL
|
|
688
|
+
)
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
# On JRuby with fiddle < 1.1.8, this relies on fiddle_compat's to_ptr
|
|
692
|
+
# (the stock one overruns its buffer by one byte).
|
|
693
|
+
def native_pointer_for(buffer)
|
|
694
|
+
Fiddle::Pointer.to_ptr(buffer)
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
def persistent_pointer_for(bytes)
|
|
698
|
+
ptr = Fiddle::Pointer.malloc(bytes.bytesize)
|
|
699
|
+
ptr[0, bytes.bytesize] = bytes
|
|
700
|
+
ptr
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
# Real COM pointers are always heap addresses far above this floor (the
|
|
704
|
+
# low 64KB of a Windows process's address space is permanently reserved
|
|
705
|
+
# and never backed by a valid allocation). Rejecting anything below it
|
|
706
|
+
# here, before the first native dereference, turns what would otherwise
|
|
707
|
+
# be an unrescuable native access violation (a JVM/process crash) on a
|
|
708
|
+
# bogus caller-supplied "pointer" into a clean Ruby TypeError.
|
|
709
|
+
MIN_PLAUSIBLE_POINTER = 0x10000
|
|
710
|
+
|
|
711
|
+
# The vtable address IS the COM object's implementation identity — every
|
|
712
|
+
# instance of the same concrete class shares one vtable. Callers that
|
|
713
|
+
# memoize a resolved Fiddle::Function should key their cache on this,
|
|
714
|
+
# not on the object's own address: after an object is released, the OS
|
|
715
|
+
# can and does reuse its heap address for an unrelated object with a
|
|
716
|
+
# different vtable, so keying by object address risks returning a
|
|
717
|
+
# function pointer resolved from the WRONG vtable for a live object
|
|
718
|
+
# that merely reused a dead one's address.
|
|
719
|
+
def vtable_address(object_addr)
|
|
720
|
+
unless object_addr.is_a?(Integer) && object_addr >= MIN_PLAUSIBLE_POINTER
|
|
721
|
+
raise TypeError, "expected a native pointer address (Integer >= #{MIN_PLAUSIBLE_POINTER}), got #{object_addr.inspect}"
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
Fiddle::Pointer.new(object_addr)[0, PTR_SIZE].unpack1(PTR_SIZE == 8 ? 'Q' : 'L')
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
def vtable_function(object_addr, index, arg_types, ret_type)
|
|
728
|
+
vtable_addr = vtable_address(object_addr)
|
|
729
|
+
func_addr = Fiddle::Pointer.new(vtable_addr)[index * PTR_SIZE, PTR_SIZE].unpack1(PTR_SIZE == 8 ? 'Q' : 'L')
|
|
730
|
+
Fiddle::Function.new(func_addr, arg_types, ret_type, STDCALL)
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
# Generic COM QueryInterface, usable against any interface pointer --
|
|
734
|
+
# every other vtable helper in this codebase is for a FIXED interface
|
|
735
|
+
# (IDispatch/ITypeInfo/ITypeLib); WIN32OLE::Event is the first caller
|
|
736
|
+
# that needs to ask an arbitrary object for an arbitrary interface.
|
|
737
|
+
def query_interface(obj_addr, iid_bytes)
|
|
738
|
+
ppv = ("\x00" * PTR_SIZE).b
|
|
739
|
+
hr = vtable_function(obj_addr, 0, [VOIDP, VOIDP, VOIDP], LONG).call(obj_addr, iid_bytes, ppv)
|
|
740
|
+
return nil if failed?(hr)
|
|
741
|
+
|
|
742
|
+
ppv.unpack1(PACK_PTR)
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
def bstr_to_s(addr)
|
|
746
|
+
return nil if addr.nil? || addr.zero?
|
|
747
|
+
|
|
748
|
+
len_bytes = Fiddle::Pointer.new(addr - 4)[0, 4].unpack1('L')
|
|
749
|
+
utf16 = Fiddle::Pointer.new(addr)[0, len_bytes]
|
|
750
|
+
# Like ole_wc2vstr (strlen after conversion): stop at the first NUL,
|
|
751
|
+
# which a BSTR's length can include.
|
|
752
|
+
nul = utf16.unpack('v*').index(0)
|
|
753
|
+
utf16_to_s(nul ? utf16[0, nul * 2] : utf16)
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
# For plain NUL-terminated wide strings (LPOLESTR/LPWSTR), unlike BSTR,
|
|
757
|
+
# which has no length prefix -- e.g. ProgIDFromCLSID's CoTaskMemAlloc'd
|
|
758
|
+
# output. Scans for the terminating UTF-16 NUL code unit.
|
|
759
|
+
def wstr_to_s(addr)
|
|
760
|
+
return nil if addr.nil? || addr.zero?
|
|
761
|
+
|
|
762
|
+
ptr = Fiddle::Pointer.new(addr)
|
|
763
|
+
len = 0
|
|
764
|
+
len += 1 while ptr[len * 2, 2].unpack1('S') != 0
|
|
765
|
+
utf16_to_s(ptr[0, len * 2])
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
def hresult_system_message(hr)
|
|
769
|
+
buf_ptr = ("\x00" * PTR_SIZE).b
|
|
770
|
+
flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS
|
|
771
|
+
count = format_message.call(flags, nil, hr, 0, buf_ptr, 0, nil)
|
|
772
|
+
return '' if count.zero?
|
|
773
|
+
|
|
774
|
+
addr = buf_ptr.unpack1(PTR_SIZE == 8 ? 'Q' : 'L')
|
|
775
|
+
msg = bstr_free_local_string(addr, count)
|
|
776
|
+
msg.chomp
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
# count is the number of UTF-16LE *characters* FormatMessageW wrote,
|
|
780
|
+
# not bytes — a factor-of-2 slip here is easy to make and only shows up
|
|
781
|
+
# once it actually runs on Windows.
|
|
782
|
+
def bstr_free_local_string(addr, count)
|
|
783
|
+
ptr = Fiddle::Pointer.new(addr)
|
|
784
|
+
msg = ptr[0, count * 2].dup.force_encoding('UTF-16LE').encode('UTF-8')
|
|
785
|
+
local_free.call(addr)
|
|
786
|
+
msg
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
def local_free
|
|
790
|
+
@local_free ||= Fiddle::Function.new(kernel32['LocalFree'], [VOIDP], VOIDP, STDCALL)
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
# var's body holds a pointer INTO realvar's own already-packed bytes --
|
|
794
|
+
# realvar must outlive var (a native address embedded as data: the
|
|
795
|
+
# caller, WIN32OLE::Variant, is what keeps realvar's String reachable
|
|
796
|
+
# for as long as var is in use).
|
|
797
|
+
# VT_VARIANT|VT_BYREF is the one exception (mirrors MRI's
|
|
798
|
+
# ole_set_byref): the pointer targets realvar's own start (offset 0,
|
|
799
|
+
# the whole VARIANT), not offset 8 (one scalar slot within it).
|
|
800
|
+
#
|
|
801
|
+
# realvar_ptr must be a persistent Fiddle::Pointer (from
|
|
802
|
+
# persistent_pointer_for), NOT a plain Ruby String -- on JRuby,
|
|
803
|
+
# Fiddle::Pointer.to_ptr(a_string) always returns a fresh, disconnected
|
|
804
|
+
# copy with no write-back, so pointing into a String's own bytes here
|
|
805
|
+
# would silently break VT_BYREF's whole purpose (a native out-parameter
|
|
806
|
+
# write becoming visible when Ruby later reads realvar).
|
|
807
|
+
def pack_byref(vt, realvar_ptr)
|
|
808
|
+
offset = vt == VT_VARIANT ? 0 : 8
|
|
809
|
+
pack_variant(vt | VT_BYREF, pack_pointer((realvar_ptr + offset).to_i))
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
def variant_change_type
|
|
813
|
+
@variant_change_type ||= Fiddle::Function.new(
|
|
814
|
+
oleaut32['VariantChangeTypeEx'], [VOIDP, VOIDP, DWORD, WORD, WORD], LONG, STDCALL
|
|
815
|
+
)
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
def variant_copy_fn
|
|
819
|
+
@variant_copy_fn ||= Fiddle::Function.new(oleaut32['VariantCopy'], [VOIDP, VOIDP], LONG, STDCALL)
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
def variant_clear_fn
|
|
823
|
+
@variant_clear_fn ||= Fiddle::Function.new(oleaut32['VariantClear'], [VOIDP], LONG, STDCALL)
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
# A VariantCopy of +variant_bytes+: its own BSTR / SAFEARRAY copy or
|
|
827
|
+
# interface reference, for variant_clear to free.
|
|
828
|
+
def variant_copy(variant_bytes)
|
|
829
|
+
dest = ("\x00" * VARIANT_SIZE).b
|
|
830
|
+
hr = variant_copy_fn.call(dest, variant_bytes)
|
|
831
|
+
raise WIN32OLE::RuntimeError, "failed to VariantCopy: #{hr_hex(hr)}" if failed?(hr)
|
|
832
|
+
|
|
833
|
+
dest
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
def variant_clear(variant)
|
|
837
|
+
variant_clear_fn.call(variant)
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
PM_REMOVE = 0x0001
|
|
841
|
+
MSG_SIZE = 64
|
|
842
|
+
|
|
843
|
+
def user32
|
|
844
|
+
@user32 ||= Fiddle.dlopen('user32')
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
def get_desktop_window
|
|
848
|
+
@get_desktop_window ||= Fiddle::Function.new(user32['GetDesktopWindow'], [], VOIDP, STDCALL)
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
HH_DISPLAY_TOPIC = 0x00
|
|
852
|
+
HH_HELP_CONTEXT = 0x0F
|
|
853
|
+
|
|
854
|
+
# HtmlHelpA from HHCTRL.OCX, or nil when that isn't available.
|
|
855
|
+
def html_help_fn
|
|
856
|
+
return @html_help_fn if defined?(@html_help_fn)
|
|
857
|
+
|
|
858
|
+
@html_help_fn =
|
|
859
|
+
begin
|
|
860
|
+
Fiddle::Function.new(Fiddle.dlopen('hhctrl.ocx')['HtmlHelpA'], [VOIDP, VOIDP, DWORD, VOIDP], VOIDP, STDCALL)
|
|
861
|
+
rescue Fiddle::DLError
|
|
862
|
+
nil
|
|
863
|
+
end
|
|
864
|
+
end
|
|
865
|
+
|
|
866
|
+
# ole_show_help: open +helpfile+ at +context+ (falling back to just the
|
|
867
|
+
# file), returning the help window handle, 0 when it couldn't.
|
|
868
|
+
def show_html_help(helpfile, context)
|
|
869
|
+
return 0 unless html_help_fn
|
|
870
|
+
|
|
871
|
+
file = "#{helpfile}\x00".b
|
|
872
|
+
desktop = get_desktop_window.call
|
|
873
|
+
hwnd = html_help_fn.call(desktop, file, HH_HELP_CONTEXT, context).to_i
|
|
874
|
+
hwnd = html_help_fn.call(desktop, file, HH_DISPLAY_TOPIC, context).to_i if hwnd.zero?
|
|
875
|
+
hwnd
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
def peek_message
|
|
879
|
+
@peek_message ||= Fiddle::Function.new(
|
|
880
|
+
user32['PeekMessageW'], [VOIDP, VOIDP, DWORD, DWORD, DWORD], DWORD, STDCALL
|
|
881
|
+
)
|
|
882
|
+
end
|
|
883
|
+
|
|
884
|
+
def translate_message
|
|
885
|
+
@translate_message ||= Fiddle::Function.new(user32['TranslateMessage'], [VOIDP], DWORD, STDCALL)
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
# DispatchMessageW returns LRESULT, which is pointer-width (64-bit on
|
|
889
|
+
# x64), not the 32-bit LONG this file uses for HRESULTs. Declaring it as
|
|
890
|
+
# VOIDP keeps the return-value width right on both x86 and x64;
|
|
891
|
+
# pump_windows_messages discards the value either way.
|
|
892
|
+
def dispatch_message
|
|
893
|
+
@dispatch_message ||= Fiddle::Function.new(user32['DispatchMessageW'], [VOIDP], VOIDP, STDCALL)
|
|
894
|
+
end
|
|
895
|
+
|
|
896
|
+
def pump_windows_messages
|
|
897
|
+
msg = Fiddle::Pointer.malloc(MSG_SIZE)
|
|
898
|
+
while peek_message.call(msg, nil, 0, 0, PM_REMOVE) != 0
|
|
899
|
+
translate_message.call(msg)
|
|
900
|
+
dispatch_message.call(msg)
|
|
901
|
+
end
|
|
902
|
+
nil
|
|
903
|
+
ensure
|
|
904
|
+
Fiddle.free(msg) if msg
|
|
905
|
+
end
|
|
906
|
+
end
|
|
907
|
+
end
|