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.
@@ -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