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,827 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fiddle'
4
+ require 'win32ole/ffi/win32'
5
+ require 'win32ole/ffi/dispatch'
6
+ require 'win32ole/ffi/array'
7
+ require 'win32ole/ffi/record'
8
+ require 'win32ole/ffi/variant'
9
+
10
+ class WIN32OLE
11
+ class RuntimeError < ::RuntimeError; end
12
+ class QueryInterfaceError < RuntimeError; end
13
+
14
+ # ext/win32ole/win32ole_error.c keeps the old toplevel names as deprecated
15
+ # aliases, so existing scripts that use them still run here but get a
16
+ # warning to move to the WIN32OLE:: names.
17
+ ::WIN32OLERuntimeError = RuntimeError
18
+ ::Object.deprecate_constant(:WIN32OLERuntimeError)
19
+
20
+ ::WIN32OLEQueryInterfaceError = QueryInterfaceError
21
+ ::Object.deprecate_constant(:WIN32OLEQueryInterfaceError)
22
+
23
+ include Dispatch
24
+
25
+ W = Win32
26
+ private_constant :W
27
+
28
+ LOCALE_SYSTEM_DEFAULT = W::LOCALE_SYSTEM_DEFAULT
29
+ LOCALE_USER_DEFAULT = W::LOCALE_USER_DEFAULT
30
+
31
+ CP_ACP = W::CP_ACP
32
+ CP_OEMCP = W::CP_OEMCP
33
+ CP_MACCP = W::CP_MACCP
34
+ CP_THREAD_ACP = W::CP_THREAD_ACP
35
+ CP_SYMBOL = W::CP_SYMBOL
36
+ CP_UTF7 = W::CP_UTF7
37
+ CP_UTF8 = W::CP_UTF8
38
+
39
+ @lcid = LOCALE_SYSTEM_DEFAULT
40
+
41
+ class << self
42
+ # ext/win32ole/win32ole.c's fole_s_get_code_page / fole_s_set_code_page:
43
+ # the codepage strings coming back from COM are converted to (initially
44
+ # derived from Encoding.default_internal || default_external).
45
+ def codepage
46
+ W.codepage
47
+ end
48
+
49
+ def codepage=(cp)
50
+ W.codepage = cp
51
+ end
52
+
53
+ def locale
54
+ @lcid
55
+ end
56
+
57
+ # ext/win32ole/win32ole.c's fole_s_set_locale: the two sentinel LCIDs
58
+ # are always accepted (they resolve dynamically at call time, so
59
+ # EnumSystemLocalesA can't confirm them up front); any other LCID must
60
+ # name an installed locale.
61
+ def locale=(lcid)
62
+ unless lcid == LOCALE_SYSTEM_DEFAULT || lcid == LOCALE_USER_DEFAULT || W.locale_installed?(lcid)
63
+ raise WIN32OLE::RuntimeError, "not installed locale: #{lcid}"
64
+ end
65
+
66
+ @lcid = lcid
67
+ nil
68
+ end
69
+
70
+ # ole_initialize (fole_s_ole_initialize): OleInitialize once per thread
71
+ # -- COM initialization is per OS thread, hence a thread variable, not
72
+ # a fiber-local Thread#[]. (C also installs an OLE message filter; this
73
+ # port doesn't.)
74
+ # Also runs the Releases finalizers left for a thread with OLE (on
75
+ # JRuby, all of them; see Win32.release_on_ole_thread).
76
+ def ole_initialize
77
+ unless Thread.current.thread_variable_get(:win32ole_ole_initialized)
78
+ hr = W.ole_initialize_fn.call(nil)
79
+ raise ::RuntimeError, "fail: OLE initialize\n#{hresult_detail(hr)}" if W.failed?(hr)
80
+
81
+ Thread.current.thread_variable_set(:win32ole_ole_initialized, true)
82
+ end
83
+ W.run_deferred_releases
84
+ nil
85
+ end
86
+
87
+ def ole_uninitialize
88
+ return nil unless Thread.current.thread_variable_get(:win32ole_ole_initialized)
89
+
90
+ W.ole_uninitialize_fn.call
91
+ Thread.current.thread_variable_set(:win32ole_ole_initialized, nil)
92
+ nil
93
+ end
94
+
95
+ # fole_s_show_help: open +target+'s help -- a help file path, or a
96
+ # WIN32OLE::Type / WIN32OLE::Method's own help file at its context.
97
+ def ole_show_help(target, helpcontext = nil)
98
+ if target.is_a?(WIN32OLE::Type) || target.is_a?(WIN32OLE::Method)
99
+ helpfile = target.helpfile
100
+ raise ::RuntimeError, "no helpfile of `#{target.name}'" if helpfile.to_s.empty?
101
+
102
+ helpcontext = target.helpcontext
103
+ else
104
+ helpfile = target
105
+ end
106
+ raise TypeError, '1st parameter must be (String|WIN32OLE::Type|WIN32OLE::Method)' unless helpfile.is_a?(::String)
107
+
108
+ hwnd = W.show_html_help(helpfile, Integer(helpcontext))
109
+ raise ::RuntimeError, "failed to open help file `#{helpfile}'" if hwnd.zero?
110
+
111
+ nil
112
+ end
113
+
114
+ # fole_s_reference_count (a debugging aid): the IDispatch's reference
115
+ # count, read with an AddRef/Release pair.
116
+ def ole_reference_count(obj)
117
+ ptr = obj.dispatch_ptr
118
+ return 0 if ptr.nil? || ptr.zero?
119
+
120
+ W.add_ref(ptr)
121
+ release_com_pointer(ptr)
122
+ end
123
+
124
+ # fole_s_free (a debugging aid): one Release while a reference is
125
+ # left, returning the new count. Unlike WIN32OLE#ole_free, the object
126
+ # keeps its pointer.
127
+ def ole_free(obj)
128
+ ptr = obj.dispatch_ptr
129
+ return 0 if ptr.nil? || ptr.zero? || ole_reference_count(obj).zero?
130
+
131
+ release_com_pointer(ptr)
132
+ end
133
+
134
+ def wrap_dispatch_pointer(ptr)
135
+ obj = allocate
136
+ obj.instance_variable_set(:@ptr, ptr)
137
+ obj.send(:install_finalizer)
138
+ obj
139
+ end
140
+
141
+ def connect(server, host = nil)
142
+ server = string_value(server)
143
+ raise NotImplementedError, 'remote OLE (host) is not supported yet' unless host.nil?
144
+
145
+ ole_initialize
146
+
147
+ # fole_s_connect: a name that is neither a ProgID nor a CLSID is
148
+ # bound as a moniker (e.g. "winmgmts:").
149
+ clsid = clsid_for(server)
150
+ return bind_moniker(server) if clsid.nil?
151
+
152
+ ppv = ("\x00" * W::PTR_SIZE).b
153
+ hr = W.get_active_object.call(clsid, nil, ppv)
154
+ if W.failed?(hr)
155
+ raise WIN32OLE::RuntimeError, "OLE server `#{server}' not running\n#{hresult_detail(hr)}"
156
+ end
157
+
158
+ # GetActiveObject's out-parameter is IUnknown**, not IDispatch** --
159
+ # ext/win32ole/win32ole.c:1948-1957 QueryInterfaces for IID_IDispatch
160
+ # and releases the original IUnknown* before wrapping the result.
161
+ iunknown_ptr = ppv.unpack1(W::PACK_PTR)
162
+ idispatch_ptr = W.query_interface(iunknown_ptr, W::IID_IDISPATCH)
163
+ release_com_pointer(iunknown_ptr)
164
+ if idispatch_ptr.nil?
165
+ raise WIN32OLE::RuntimeError, W.unknown_server_error_message(server)
166
+ end
167
+
168
+ wrap_dispatch_pointer(idispatch_ptr)
169
+ end
170
+
171
+ def release_com_pointer(ptr)
172
+ return if ptr.nil? || ptr.zero?
173
+
174
+ W.vtable_function(ptr, 2, [W::VOIDP], W::DWORD).call(ptr)
175
+ end
176
+
177
+ # +to_free+ collects a proc per allocation behind the VARIANT (C's
178
+ # VariantClear of it); call them once the VARIANT is no longer needed.
179
+ def ruby_value_to_variant_bytes(value, to_free)
180
+ case value
181
+ when ::Array
182
+ psa = SafeArray.ruby_array_to_safearray(value, W::VT_VARIANT, to_free)
183
+ to_free << -> { SafeArray.safe_array_destroy.call(psa) }
184
+ return W.pack_variant(W::VT_VARIANT | W::VT_ARRAY, W.pack_pointer(psa.to_i))
185
+ when WIN32OLE::Record
186
+ return value.to_variant_bytes
187
+ when WIN32OLE::Variant
188
+ # ole_variant2variant
189
+ copy = W.variant_copy(value.instance_variable_get(:@var))
190
+ to_free << -> { W.variant_clear(copy) }
191
+ return copy
192
+ end
193
+
194
+ type = W.ruby_to_variant_type(value)
195
+ payload =
196
+ case type
197
+ when :i4 then W.pack_i4(value)
198
+ when :i8 then W.pack_i8(value)
199
+ when :r8 then W.pack_r8(value)
200
+ when :bool then W.pack_bool(value)
201
+ when :empty then W.pack_empty
202
+ when :bstr
203
+ bstr = W.sys_alloc_string.call(W.wstr(value))
204
+ to_free << -> { W.sys_free_string.call(bstr) }
205
+ W.pack_pointer(bstr)
206
+ when :dispatch
207
+ ptr = value.instance_variable_get(:@ptr)
208
+ if ptr && !ptr.zero?
209
+ W.add_ref(ptr)
210
+ to_free << -> { release_com_pointer(ptr) }
211
+ end
212
+ W.pack_pointer(ptr)
213
+ when :date
214
+ W.pack_r8(W.time_to_vtdate(value))
215
+ end
216
+ W.pack_variant(W::VT_FOR_TYPE.fetch(type), payload)
217
+ end
218
+
219
+ # ole_variant2val. owned: true (a call's own result) hands its BSTR /
220
+ # IDispatch reference over to the Ruby value; owned: false (a
221
+ # Variant's own value, a SAFEARRAY element, anything behind VT_BYREF)
222
+ # leaves the VARIANT intact -- the BSTR is not freed and the IDispatch
223
+ # is AddRef'd before being wrapped.
224
+ def variant_bytes_to_ruby_value(bytes, owned: true)
225
+ vt, = W.unpack_variant(bytes)
226
+ if (vt & W::VT_BYREF) != 0
227
+ owned = false
228
+ bytes = W.deref_variant(bytes)
229
+ vt, = W.unpack_variant(bytes)
230
+ end
231
+ base_vt = vt & W::VT_TYPEMASK
232
+
233
+ if (vt & W::VT_ARRAY) != 0
234
+ _vt, payload = W.unpack_variant(bytes)
235
+ psa = W.unpack_pointer(payload)
236
+ return nil if psa.zero?
237
+
238
+ return SafeArray.safearray_to_ruby_array(psa, base_vt)
239
+ end
240
+
241
+ if base_vt == W::VT_RECORD
242
+ _vt, body = W.unpack_variant(bytes, body_size: WIN32OLE::Record::VT_RECORD_BODY_SIZE)
243
+ buffer_ptr, pri = body.unpack("#{W::PACK_PTR}2")
244
+ return WIN32OLE::Record.from_irecordinfo_and_buffer(pri, buffer_ptr)
245
+ end
246
+
247
+ type = W.variant_ruby_type(vt)
248
+ _vt2, payload = W.unpack_variant(bytes)
249
+ case type
250
+ when :empty then nil
251
+ when :i4 then W.unpack_i4(payload)
252
+ when :i8 then W.unpack_i8(payload)
253
+ when :r8 then W.unpack_r8(payload)
254
+ when :bool then W.unpack_bool(payload)
255
+ when :i1 then W.unpack_i1(payload)
256
+ when :ui1 then W.unpack_ui1(payload)
257
+ when :i2 then W.unpack_i2(payload)
258
+ when :ui2 then W.unpack_ui2(payload)
259
+ when :ui4 then W.unpack_ui4(payload)
260
+ when :ui8 then W.unpack_ui8(payload)
261
+ when :int then W.unpack_int(payload)
262
+ when :uint then W.unpack_uint(payload)
263
+ when :r4 then W.unpack_r4(payload)
264
+ when :error then W.unpack_error(payload)
265
+ when :date then W.vtdate_to_time(W.unpack_r8(payload))
266
+ when :bstr
267
+ addr = W.unpack_pointer(payload)
268
+ str = W.bstr_to_s(addr) || '' # a NULL BSTR is "", as in ole_variant2val
269
+ W.sys_free_string.call(addr) if owned && !addr.zero?
270
+ str
271
+ when :dispatch
272
+ ptr = W.unpack_pointer(payload)
273
+ unless ptr.zero?
274
+ W.add_ref(ptr) unless owned
275
+ wrap_dispatch_pointer(ptr)
276
+ end
277
+ when :other then variant_to_bstr_value(bytes)
278
+ end
279
+ end
280
+
281
+ # ole_variant2val's default branch (VT_CY, VT_DECIMAL, ...): the
282
+ # VariantChangeTypeEx(VT_BSTR) rendering, or nil if it can't convert.
283
+ def variant_to_bstr_value(bytes)
284
+ dest = ("\x00" * W::VARIANT_SIZE).b
285
+ hr = W.variant_change_type.call(dest, bytes, locale, 0, W::VT_BSTR)
286
+ return nil if W.failed?(hr)
287
+
288
+ addr = W.unpack_pointer(W.unpack_variant(dest)[1])
289
+ str = W.bstr_to_s(addr) || ''
290
+ W.sys_free_string.call(addr) unless addr.zero?
291
+ str
292
+ end
293
+
294
+ def resolve_clsid(server)
295
+ clsid, hr = clsid_and_hr(server)
296
+ return clsid if clsid
297
+
298
+ raise WIN32OLE::RuntimeError, "#{W.unknown_server_error_message(server)}\n#{hresult_detail(hr)}"
299
+ end
300
+
301
+ # The CLSID for a ProgID or "{...}" CLSID string, or nil.
302
+ def clsid_for(server)
303
+ clsid_and_hr(server).first
304
+ end
305
+
306
+ def clsid_and_hr(server)
307
+ wide = W.wstr(server)
308
+ clsid = ("\x00" * 16).b
309
+ hr = W.clsid_from_progid.call(wide, clsid)
310
+ # ole_create_dcom tries the remote host's registry in between.
311
+ if W.failed?(hr) && block_given? && (remote = yield)
312
+ return [remote, 0]
313
+ end
314
+ hr = W.clsid_from_string.call(wide, clsid) if W.failed?(hr)
315
+ [W.failed?(hr) ? nil : clsid, hr]
316
+ end
317
+
318
+ IMONIKER_BIND_TO_OBJECT = 8 # IUnknown(3) + IPersist(1) + IPersistStream(4)
319
+
320
+ # ole_bind_obj: CreateBindCtx + MkParseDisplayName + IMoniker::BindToObject
321
+ # for IDispatch.
322
+ def bind_moniker(moniker)
323
+ out = ("\x00" * W::PTR_SIZE).b
324
+ hr = W.create_bind_ctx.call(0, out)
325
+ raise WIN32OLE::RuntimeError, "failed to create bind context\n#{hresult_detail(hr)}" if W.failed?(hr)
326
+
327
+ bindctx = out.unpack1(W::PACK_PTR)
328
+ begin
329
+ eaten = ("\x00" * 4).b
330
+ out = ("\x00" * W::PTR_SIZE).b
331
+ hr = W.mk_parse_display_name.call(bindctx, W.wstr(moniker), eaten, out)
332
+ if W.failed?(hr)
333
+ raise WIN32OLE::RuntimeError, "failed to parse display name of moniker `#{moniker}'\n#{hresult_detail(hr)}"
334
+ end
335
+
336
+ pmoniker = out.unpack1(W::PACK_PTR)
337
+ begin
338
+ out = ("\x00" * W::PTR_SIZE).b
339
+ hr = W.vtable_function(pmoniker, IMONIKER_BIND_TO_OBJECT, [W::VOIDP] * 5, W::LONG)
340
+ .call(pmoniker, bindctx, nil, W::IID_IDISPATCH, out)
341
+ raise WIN32OLE::RuntimeError, "failed to bind moniker `#{moniker}'\n#{hresult_detail(hr)}" if W.failed?(hr)
342
+ ensure
343
+ release_com_pointer(pmoniker)
344
+ end
345
+ ensure
346
+ release_com_pointer(bindctx)
347
+ end
348
+ wrap_dispatch_pointer(out.unpack1(W::PACK_PTR))
349
+ end
350
+
351
+ def hresult_detail(hr)
352
+ " HRESULT error code:#{W.hr_hex(hr)}\n #{W.hresult_system_message(hr)}"
353
+ end
354
+
355
+ def create_guid
356
+ buf = ("\x00" * 16).b
357
+ hr = W.co_create_guid.call(buf)
358
+ raise WIN32OLE::RuntimeError, "failed to create GUID\n#{hresult_detail(hr)}" if W.failed?(hr)
359
+
360
+ d1, d2, d3 = buf.unpack('LSS')
361
+ d4 = buf[8, 8].unpack('C8')
362
+ format('{%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}', d1, d2, d3, *d4)
363
+ end
364
+
365
+ # ext/win32ole/win32ole.c's fole_s_const_load / ole_const_load. +ole+ is
366
+ # a WIN32OLE (its containing typelib) or a String naming a typelib the
367
+ # way typelib_file resolves it; +mod+ defaults to WIN32OLE itself. Like
368
+ # C, each visible constant's first letter is ASCII-upcased
369
+ # (ssfWINDOWS -> SsfWINDOWS); names that still aren't valid constant
370
+ # names go into a CONSTANTS hash on +mod+ instead.
371
+ def const_load(ole, mod = nil)
372
+ raise TypeError, '2nd parameter must be Class or Module' unless mod.nil? || mod.is_a?(::Module)
373
+
374
+ mod ||= self
375
+ tlib =
376
+ case ole
377
+ when WIN32OLE then ole.ole_type.ole_typelib
378
+ when ::String
379
+ ptr = WIN32OLE::TypeLib.load_typelib(WIN32OLE::TypeLib.file_for(ole) || ole)
380
+ raise WIN32OLE::RuntimeError, 'failed to LoadTypeLibEx' if ptr.nil?
381
+
382
+ WIN32OLE::TypeLib.from_itypelib_ptr(ptr)
383
+ else
384
+ raise TypeError, '1st parameter must be WIN32OLE instance'
385
+ end
386
+
387
+ constants = {}
388
+ tlib.ole_types.each do |type|
389
+ type.variables.each do |var|
390
+ next unless var.variable_kind == 'CONSTANT' && var.visible?
391
+
392
+ name = var.name.dup
393
+ name[0] = name[0].upcase(:ascii)
394
+ if name.match?(/\A[A-Z]\w*\z/)
395
+ mod.const_set(name, var.value) unless mod.const_defined?(name, false)
396
+ else
397
+ constants[name] = var.value
398
+ end
399
+ end
400
+ end
401
+ mod.const_set(:CONSTANTS, constants)
402
+ nil
403
+ end
404
+
405
+ # Ruby-level StringValue: a String, or anything with an implicit
406
+ # #to_str, else TypeError -- which is what ext/win32ole raises for a
407
+ # non-string server/host/method name.
408
+ def string_value(val)
409
+ String.try_convert(val) || raise(TypeError, "no implicit conversion of #{val.class} into String")
410
+ end
411
+
412
+ private :resolve_clsid, :clsid_for, :clsid_and_hr, :bind_moniker, :hresult_detail, :release_com_pointer,
413
+ :string_value
414
+ end
415
+
416
+ def initialize(server, host = nil)
417
+ server = self.class.send(:string_value, server)
418
+ self.class.ole_initialize
419
+ return create_dcom(server, self.class.send(:string_value, host)) unless host.nil?
420
+
421
+ clsid = self.class.send(:resolve_clsid, server)
422
+ ppv = ("\x00" * W::PTR_SIZE).b
423
+ hr = W.co_create_instance.call(
424
+ clsid, nil, W::CLSCTX_INPROC_SERVER | W::CLSCTX_LOCAL_SERVER, W::IID_IDISPATCH, ppv
425
+ )
426
+ if W.failed?(hr)
427
+ raise WIN32OLE::RuntimeError, "#{W.unknown_server_error_message(server)}\n#{self.class.send(:hresult_detail, hr)}"
428
+ end
429
+
430
+ @ptr = ppv.unpack1(W::PTR_SIZE == 8 ? 'Q' : 'L')
431
+ install_finalizer
432
+ end
433
+
434
+ private
435
+
436
+ # ole_create_dcom: the CLSID from the ProgID here, then from the host's
437
+ # registry, then as a CLSID string; the object from CoCreateInstanceEx
438
+ # on +host+.
439
+ def create_dcom(server, host)
440
+ clsid, hr = self.class.send(:clsid_and_hr, server) { W.clsid_from_remote(host, server) }
441
+ raise WIN32OLE::RuntimeError, "#{W.unknown_server_error_message(server)}\n#{self.class.send(:hresult_detail, hr)}" unless clsid
442
+
443
+ # The pointers, not just their addresses, are held until the call: on
444
+ # JRuby to_ptr copies a String into native memory that is freed once its
445
+ # Fiddle::Pointer is collected.
446
+ host_name = W.wstr(host)
447
+ host_name_ptr = W.native_pointer_for(host_name)
448
+ # COSERVERINFO { DWORD dwReserved1; LPWSTR pwszName; COAUTHINFO *pAuthInfo; DWORD dwReserved2; }
449
+ info = [0, host_name_ptr.to_i, 0, 0].pack("L#{'x4' if W::PTR_SIZE == 8}#{W::PACK_PTR}#{W::PACK_PTR}L#{'x4' if W::PTR_SIZE == 8}")
450
+ iid = W::IID_IDISPATCH.dup
451
+ iid_ptr = W.native_pointer_for(iid)
452
+ # MULTI_QI { const IID *pIID; IUnknown *pItf; HRESULT hr; }
453
+ mqi = [iid_ptr.to_i, 0, 0].pack("#{W::PACK_PTR}#{W::PACK_PTR}l#{'x4' if W::PTR_SIZE == 8}")
454
+ hr = W.co_create_instance_ex.call(clsid, nil, W::CLSCTX_REMOTE_SERVER, info, 1, mqi)
455
+ if W.failed?(hr)
456
+ raise WIN32OLE::RuntimeError,
457
+ "failed to create DCOM server `#{server}' in `#{host}'\n#{self.class.send(:hresult_detail, hr)}"
458
+ end
459
+
460
+ @ptr = mqi[W::PTR_SIZE, W::PTR_SIZE].unpack1(W::PACK_PTR)
461
+ install_finalizer
462
+ end
463
+
464
+ def install_finalizer
465
+ ptr = @ptr
466
+ release_fn = W.vtable_function(ptr, 2, [W::VOIDP], W::DWORD)
467
+ ObjectSpace.define_finalizer(self, self.class.finalizer(ptr, release_fn))
468
+ end
469
+
470
+ # OLE_FREE: Release only on a thread that has initialized OLE. GC can run
471
+ # this on any thread, e.g. the main thread at exit after the apartment the
472
+ # object lived in is gone; then a later thread with OLE does it. On JRuby
473
+ # that is every time: it runs all finalizers on a thread of its own, which
474
+ # never has OLE.
475
+ def self.finalizer(ptr, release_fn)
476
+ proc { W.release_on_ole_thread { release_fn.call(ptr) } unless ptr.zero? }
477
+ end
478
+
479
+ DISP_E_EXCEPTION = -2147352567 # 0x80020009
480
+
481
+ # A Symbol (the normal path) or a String is accepted, as fole_missing's
482
+ # rb_check_symbol + StringValueCStr does; anything else is a TypeError.
483
+ def method_missing(name, *args)
484
+ mname = name.is_a?(::Symbol) ? name.to_s : self.class.send(:string_value, name)
485
+ plan = W.dispatch_plan(mname, args)
486
+ dispid = dispid_for(plan[:name])
487
+ if dispid.nil?
488
+ if plan[:named_put]
489
+ raise WIN32OLE::RuntimeError, W.property_put_error_message(plan[:name], "\nunknown property or method: `#{plan[:name]}'")
490
+ end
491
+ return super(name, *args)
492
+ end
493
+
494
+ args, named_args = split_named_args(plan[:name], args) unless plan[:named_put]
495
+ hr, result_bytes, excepinfo_bytes =
496
+ ole_invoke(dispid, args, plan[:wflags], named_put: plan[:named_put], named_args: named_args)
497
+
498
+ if W.failed?(hr)
499
+ detail = error_detail(hr, excepinfo_bytes)
500
+ message = plan[:named_put] ? W.property_put_error_message(plan[:name], detail)
501
+ : W.method_error_message(plan[:name], detail)
502
+ raise WIN32OLE::RuntimeError, message
503
+ end
504
+
505
+ return nil if plan[:named_put]
506
+
507
+ self.class.variant_bytes_to_ruby_value(result_bytes)
508
+ end
509
+ # Public, as rb_define_method makes fole_missing.
510
+ public :method_missing
511
+
512
+ def respond_to_missing?(name, include_private = false)
513
+ !dispid_for(name.to_s.sub(/=\z/, '')).nil? || super
514
+ end
515
+
516
+ public
517
+
518
+ # ext/win32ole/win32ole.c's fole_invoke: the explicit-call form of
519
+ # method_missing's regular (non property-put) path -- lets a caller
520
+ # invoke a method whose name collides with a Ruby method (Object#send
521
+ # works too, but this matches MRI's documented API).
522
+ def invoke(name, *args)
523
+ dispid = dispid_for(name.to_s)
524
+ return super if dispid.nil?
525
+
526
+ args, named_args = split_named_args(name.to_s, args)
527
+ hr, result_bytes, excepinfo_bytes =
528
+ ole_invoke(dispid, args, W::DISPATCH_METHOD | W::DISPATCH_PROPERTYGET, named_args: named_args)
529
+
530
+ if W.failed?(hr)
531
+ raise WIN32OLE::RuntimeError, W.method_error_message(name, error_detail(hr, excepinfo_bytes))
532
+ end
533
+
534
+ self.class.variant_bytes_to_ruby_value(result_bytes)
535
+ end
536
+
537
+ # ext/win32ole/win32ole.c's fole_invoke2/fole_getproperty2/fole_setproperty2:
538
+ # the "early binding" siblings of #invoke/#setproperty -- dispid and
539
+ # per-argument VARTYPE are given explicitly instead of resolved by name,
540
+ # so no GetIDsOfNames round-trip happens.
541
+ def _invoke(dispid, args, types)
542
+ ole_invoke2(dispid, args, types, W::DISPATCH_METHOD)
543
+ end
544
+
545
+ def _getproperty(dispid, args, types)
546
+ ole_invoke2(dispid, args, types, W::DISPATCH_PROPERTYGET)
547
+ end
548
+
549
+ def _setproperty(dispid, args, types)
550
+ ole_invoke2(dispid, args, types, W::DISPATCH_PROPERTYPUT)
551
+ end
552
+
553
+ # ext/win32ole/win32ole.c's fole_setproperty: like the `name=(val)`
554
+ # method_missing path, but for properties that also take index
555
+ # arguments (e.g. sheet.setproperty('Cells', 1, 2, 10)) -- the last
556
+ # argument is the value, everything before it is an index arg, and (per
557
+ # ole_invoke) they're all invoked as one DISPATCH_PROPERTYPUT call with
558
+ # the value carried as the DISPID_PROPERTYPUT named argument.
559
+ def setproperty(name, *args)
560
+ if args.empty?
561
+ raise WIN32OLE::RuntimeError, W.property_put_error_message(name, "\nargument error")
562
+ end
563
+
564
+ dispid = dispid_for(name.to_s)
565
+ return super if dispid.nil?
566
+
567
+ hr, _result_bytes, excepinfo_bytes = ole_invoke(dispid, args, W::DISPATCH_PROPERTYPUT, named_put: true)
568
+
569
+ if W.failed?(hr)
570
+ raise WIN32OLE::RuntimeError, W.property_put_error_message(name, error_detail(hr, excepinfo_bytes))
571
+ end
572
+
573
+ nil
574
+ end
575
+
576
+ # ext/win32ole/win32ole.c's fole_each: gets the collection's enumerator
577
+ # through DISPID_NEWENUM, QueryInterfaces it for IEnumVARIANT, and yields
578
+ # each item IEnumVARIANT::Next returns until it stops returning S_OK.
579
+ def each
580
+ return to_enum(:each) unless block_given?
581
+
582
+ hr, result_bytes, = ole_invoke(W::DISPID_NEWENUM, [], W::DISPATCH_METHOD | W::DISPATCH_PROPERTYGET)
583
+ if W.failed?(hr)
584
+ raise WIN32OLE::QueryInterfaceError, "failed to get IEnum Interface\n#{self.class.send(:hresult_detail, hr)}"
585
+ end
586
+
587
+ vt, payload = W.unpack_variant(result_bytes)
588
+ unknown = [W::VT_UNKNOWN, W::VT_DISPATCH].include?(vt) ? W.unpack_pointer(payload) : 0
589
+ raise WIN32OLE::QueryInterfaceError, 'failed to get IEnum Interface' if unknown.zero?
590
+
591
+ enum = W.query_interface(unknown, W::IID_IENUMVARIANT)
592
+ self.class.send(:release_com_pointer, unknown)
593
+ raise WIN32OLE::QueryInterfaceError, 'failed to get IEnum Interface' if enum.nil?
594
+
595
+ begin
596
+ next_fn = W.vtable_function(enum, 3, [W::VOIDP, W::DWORD, W::VOIDP, W::VOIDP], W::LONG)
597
+ loop do
598
+ var = ("\x00" * W::VARIANT_SIZE).b
599
+ break unless next_fn.call(enum, 1, var, nil).zero? # S_OK; S_FALSE (1) = no more
600
+
601
+ yield self.class.variant_bytes_to_ruby_value(var)
602
+ end
603
+ ensure
604
+ self.class.send(:release_com_pointer, enum)
605
+ end
606
+ nil
607
+ end
608
+
609
+ # ext/win32ole/win32ole.c's fole_getproperty_with_bracket: invokes the
610
+ # object's default member (DISPID_VALUE) with the given arguments --
611
+ # e.g. dict['ruby'] on a Scripting.Dictionary, whose default property
612
+ # is Item. No name lookup happens here (unlike method_missing/#invoke):
613
+ # all arguments are passed straight through to DISPID_VALUE.
614
+ def [](*args)
615
+ hr, result_bytes, excepinfo_bytes = ole_invoke(W::DISPID_VALUE, args, W::DISPATCH_PROPERTYGET)
616
+ if W.failed?(hr)
617
+ raise WIN32OLE::RuntimeError, W.method_error_message(args.first, error_detail(hr, excepinfo_bytes))
618
+ end
619
+
620
+ self.class.variant_bytes_to_ruby_value(result_bytes)
621
+ end
622
+
623
+ # ext/win32ole/win32ole.c's fole_setproperty_with_bracket: same
624
+ # DISPID_VALUE target as #[], but DISPATCH_PROPERTYPUT -- the last
625
+ # argument is the value, everything before it is an index argument
626
+ # (e.g. dict[2] = 'TWO').
627
+ def []=(*args)
628
+ hr, _result_bytes, excepinfo_bytes = ole_invoke(W::DISPID_VALUE, args, W::DISPATCH_PROPERTYPUT, named_put: true)
629
+ if W.failed?(hr)
630
+ raise WIN32OLE::RuntimeError, W.property_put_error_message(args.first, error_detail(hr, excepinfo_bytes))
631
+ end
632
+
633
+ args.last
634
+ end
635
+
636
+ IPERSISTMEMORY_INIT_NEW = 8 # IUnknown(3) + IPersist(1) + IsDirty/Load/Save/GetSizeMax
637
+
638
+ # fole_activex_initialize: initialize an ActiveX control through its
639
+ # IPersistMemory::InitNew.
640
+ def ole_activex_initialize
641
+ persist = W.query_interface(@ptr, W::IID_IPERSISTMEMORY)
642
+ hr = -2147467262 # E_NOINTERFACE
643
+ if persist
644
+ begin
645
+ hr = W.vtable_function(persist, IPERSISTMEMORY_INIT_NEW, [W::VOIDP], W::LONG).call(persist)
646
+ ensure
647
+ self.class.send(:release_com_pointer, persist)
648
+ end
649
+ end
650
+ if W.failed?(hr)
651
+ raise WIN32OLE::RuntimeError, "fail to initialize ActiveX control\n#{self.class.send(:hresult_detail, hr)}"
652
+ end
653
+
654
+ nil
655
+ end
656
+
657
+ # fole_free: Release the IDispatch now instead of at GC time.
658
+ def ole_free
659
+ release
660
+ nil
661
+ end
662
+
663
+ # WIN32OLE::Event needs this raw IDispatch* to build its own COM
664
+ # connections (QueryInterface for IConnectionPointContainer,
665
+ # IProvideClassInfo2, etc.) -- #ole_query_interface wraps whatever it
666
+ # gets back as a WIN32OLE, which is not what Event needs, so this is
667
+ # Event's one deliberate, documented crack in encapsulation.
668
+ def dispatch_ptr
669
+ @ptr
670
+ end
671
+
672
+ def ole_type
673
+ type_info_ptr = get_type_info_ptr
674
+ raise WIN32OLE::QueryInterfaceError, 'failed to GetTypeInfo' if type_info_ptr.nil?
675
+
676
+ Type.from_typeinfo_ptr(type_info_ptr)
677
+ end
678
+
679
+ def ole_typelib
680
+ ole_type.ole_typelib
681
+ end
682
+
683
+ def ole_methods
684
+ ole_methods_by_invkind(nil)
685
+ end
686
+
687
+ def ole_get_methods
688
+ ole_methods_by_invkind(TypeInfo::INVOKE_PROPERTYGET)
689
+ end
690
+
691
+ def ole_put_methods
692
+ ole_methods_by_invkind(TypeInfo::INVOKE_PROPERTYPUT | TypeInfo::INVOKE_PROPERTYPUTREF)
693
+ end
694
+
695
+ def ole_func_methods
696
+ ole_methods_by_invkind(TypeInfo::INVOKE_FUNC)
697
+ end
698
+
699
+ def ole_respond_to?(name)
700
+ !dispid_for(name.to_s).nil?
701
+ end
702
+
703
+ # fole_method_help: the WIN32OLE::Method named +name+ on this object's
704
+ # type. (ole_method_help is an alias of it, and ole_obj_help one of
705
+ # ole_type -- neither launches a help file; only WIN32OLE.ole_show_help
706
+ # does.)
707
+ def ole_method(name)
708
+ WIN32OLE::Method.new(type_via_containing_typelib, self.class.send(:string_value, name))
709
+ end
710
+ alias ole_method_help ole_method
711
+ alias ole_obj_help ole_type
712
+
713
+ # ext/win32ole/win32ole.c's fole_query_interface: like C, the returned
714
+ # interface pointer is wrapped as a WIN32OLE (i.e. treated as IDispatch*).
715
+ def ole_query_interface(iid)
716
+ iid = self.class.send(:string_value, iid)
717
+ raise WIN32OLE::RuntimeError, 'this WIN32OLE object has already been released' if @ptr.nil? || @ptr.zero?
718
+
719
+ iid_bytes = ("\x00" * 16).b
720
+ hr = W.clsid_from_string.call(W.wstr(iid), iid_bytes)
721
+ if W.failed?(hr)
722
+ raise WIN32OLE::RuntimeError, "invalid iid: `#{iid}'\n#{self.class.send(:hresult_detail, hr)}"
723
+ end
724
+
725
+ ppv = ("\x00" * W::PTR_SIZE).b
726
+ hr = W.vtable_function(@ptr, 0, [W::VOIDP, W::VOIDP, W::VOIDP], W::LONG).call(@ptr, iid_bytes, ppv)
727
+ if W.failed?(hr)
728
+ raise WIN32OLE::QueryInterfaceError, "failed to get interface `#{iid}'\n#{self.class.send(:hresult_detail, hr)}"
729
+ end
730
+
731
+ self.class.wrap_dispatch_pointer(ppv.unpack1(W::PACK_PTR))
732
+ end
733
+
734
+ private
735
+
736
+ # ext/win32ole/win32ole.c's ole_invoke2: builds one DISPPARAMS entry per
737
+ # (value, VARTYPE) pair -- VT_VARIANT means "pass as its own natural
738
+ # type" (no coercion), matching C's skip of VariantChangeTypeEx for that
739
+ # case; anything else is packed via WIN32OLE::Variant.new(val, vt),
740
+ # which already implements the explicit-VARTYPE coercion (including
741
+ # VT_ARRAY/VT_BYREF) that this needs. The Variant objects are kept alive
742
+ # in keep_alive through the Invoke call, since VT_BYREF's backing buffer
743
+ # is otherwise only referenced from their own instance state.
744
+ def ole_invoke2(dispid, args, types, dispkind)
745
+ raise WIN32OLE::RuntimeError, 'this WIN32OLE object has already been released' if @ptr.nil? || @ptr.zero?
746
+ raise TypeError, 'wrong argument type (expected Array)' unless args.is_a?(::Array) && types.is_a?(::Array)
747
+
748
+ to_free = []
749
+ keep_alive = []
750
+ arg_variants = args.zip(types).map do |val, vt|
751
+ if vt == W::VT_VARIANT
752
+ WIN32OLE.ruby_value_to_variant_bytes(val, to_free)
753
+ else
754
+ variant = WIN32OLE::Variant.new(val, vt)
755
+ keep_alive << variant
756
+ variant.instance_variable_get(:@var)
757
+ end
758
+ end.reverse
759
+ args_blob = arg_variants.join
760
+ args_ptr = args_blob.empty? ? nil : W.native_pointer_for(args_blob)
761
+
762
+ named_put = (dispkind & W::DISPATCH_PROPERTYPUT) != 0
763
+ named_blob = named_put ? [W::DISPID_PROPERTYPUT].pack('l') : ''
764
+ named_ptr = named_blob.empty? ? nil : W.native_pointer_for(named_blob)
765
+
766
+ dispparams = [
767
+ args_ptr ? args_ptr.to_i : 0,
768
+ named_ptr ? named_ptr.to_i : 0,
769
+ arg_variants.size,
770
+ named_put ? 1 : 0
771
+ ].pack("#{W::PACK_PTR}#{W::PACK_PTR}LL")
772
+
773
+ excepinfo = ("\x00" * W::EXCEPINFO_SIZE).b
774
+ result = ("\x00" * W::VARIANT_SIZE).b
775
+
776
+ hr = invoke_fn.call(@ptr, dispid, W::IID_NULL, 0, dispkind, dispparams, result, excepinfo, nil)
777
+ to_free.each(&:call)
778
+
779
+ if W.failed?(hr)
780
+ raise WIN32OLE::RuntimeError, W.method_error_message("<dispatch id:#{dispid}>", error_detail(hr, excepinfo))
781
+ end
782
+
783
+ self.class.variant_bytes_to_ruby_value(result)
784
+ end
785
+
786
+ def error_detail(hr, excepinfo_bytes)
787
+ if hr == DISP_E_EXCEPTION
788
+ info = W.parse_excepinfo(excepinfo_bytes)
789
+ source = W.bstr_to_s(info[:bstr_source_ptr]) || '<Unknown>'
790
+ description = W.bstr_to_s(info[:bstr_description_ptr]) || '<No Description>'
791
+ code = info[:w_code].zero? ? (info[:scode] & 0xFFFFFFFF).to_s(16).upcase : info[:w_code].to_s
792
+ "\n OLE error code:#{code} in #{source}\n #{description}\n#{self.class.send(:hresult_detail, hr)}"
793
+ else
794
+ "\n#{self.class.send(:hresult_detail, hr)}"
795
+ end
796
+ end
797
+
798
+ def get_type_info_ptr
799
+ out = ("\x00" * W::PTR_SIZE).b
800
+ hr = TypeInfo.get_type_info_fn(@ptr).call(@ptr, 0, W::LOCALE_SYSTEM_DEFAULT, out)
801
+ return nil if W.failed?(hr)
802
+
803
+ out.unpack1(W::PACK_PTR)
804
+ end
805
+
806
+ def ole_methods_by_invkind(mask)
807
+ type_via_containing_typelib.ole_methods.select do |m|
808
+ mask.nil? || (m.invkind & mask) != 0
809
+ end
810
+ end
811
+
812
+ # Ports ext/win32ole/win32ole.c's typeinfo_from_ole: GetTypeInfo →
813
+ # GetDocumentation (this type's own name) → GetContainingTypeLib →
814
+ # scan the typelib for the entry with a matching name → GetTypeInfo(i)
815
+ # again. Why MRI does this round-trip instead of just reusing the first
816
+ # ITypeInfo* the way #ole_type does is not confirmed (most likely
817
+ # GetTypeInfo(0, ...) can return a coclass's type info rather than the
818
+ # dispinterface's) — ported as-is rather than "simplified" without
819
+ # understanding it.
820
+ def type_via_containing_typelib
821
+ first_type = ole_type
822
+ target_name = first_type.name
823
+ tlib = first_type.ole_typelib
824
+ match = tlib.ole_types.find { |t| t.name == target_name }
825
+ match || first_type
826
+ end
827
+ end