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,800 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'win32ole/ffi/win32'
4
+ require 'win32ole/ffi/typeinfo'
5
+ require 'win32ole/ffi/win32ole'
6
+
7
+ class WIN32OLE
8
+ class Event
9
+ W = Win32
10
+ TI = TypeInfo
11
+ private_constant :W, :TI
12
+
13
+ def initialize(ole, itf = nil)
14
+ raise TypeError, '1st parameter must be WIN32OLE object' unless ole.is_a?(WIN32OLE)
15
+
16
+ @events = []
17
+ @handler = nil
18
+ @finalizer_state = nil
19
+ @sink_closures = nil
20
+ @event_typeinfo_ptr = nil
21
+ @ole = nil
22
+
23
+ advise(ole, itf)
24
+ end
25
+
26
+ def self.message_loop
27
+ W.pump_windows_messages
28
+ end
29
+
30
+ def on_event(event = nil, &block)
31
+ register_event(event, block, false)
32
+ end
33
+
34
+ def on_event_with_outargs(event = nil, &block)
35
+ register_event(event, block, true)
36
+ end
37
+
38
+ def off_event(event = nil)
39
+ name = event.nil? ? nil : normalize_event_name(event)
40
+ @events.reject! { |e| e[:name] == name }
41
+ nil
42
+ end
43
+
44
+ def handler=(obj)
45
+ @handler = obj
46
+ end
47
+
48
+ def handler
49
+ @handler
50
+ end
51
+
52
+ def unadvise
53
+ return nil if @finalizer_state.nil? || @finalizer_state[:cp_ptr].nil?
54
+
55
+ # ext/win32ole/win32ole_event.c:1164 (fev_unadvise): drain the message
56
+ # queue BEFORE Unadvise, so an already-queued event can't fire into a
57
+ # sink we are about to tear down. Deliberately NOT done in
58
+ # self.finalizer -- a finalizer runs at GC time with no well-defined
59
+ # thread or message-loop context.
60
+ W.pump_windows_messages
61
+
62
+ cp_ptr = @finalizer_state[:cp_ptr]
63
+ W.vtable_function(cp_ptr, 6, [W::VOIDP, W::DWORD], W::LONG).call(cp_ptr, @finalizer_state[:cookie])
64
+ W.vtable_function(cp_ptr, 2, [W::VOIDP], W::DWORD).call(cp_ptr)
65
+ release_ptr(@finalizer_state[:disp_ptr])
66
+ ti_ptr = @finalizer_state[:ti_ptr]
67
+ W.vtable_function(ti_ptr, 2, [W::VOIDP], W::DWORD).call(ti_ptr) if ti_ptr && !ti_ptr.zero?
68
+ Fiddle.free(Fiddle::Pointer.new(@finalizer_state[:sink_addr])) if @finalizer_state[:sink_addr]
69
+ Fiddle.free(Fiddle::Pointer.new(@finalizer_state[:vtable_addr])) if @finalizer_state[:vtable_addr]
70
+
71
+ @finalizer_state[:cp_ptr] = nil
72
+ @finalizer_state[:disp_ptr] = nil
73
+ @finalizer_state[:ti_ptr] = nil
74
+ @finalizer_state[:sink_addr] = nil
75
+ @finalizer_state[:vtable_addr] = nil
76
+ @finalizer_state[:iunknown_closures] = nil
77
+ @sink_closures = nil
78
+ @event_typeinfo_ptr = nil
79
+ ADVISED.delete(self)
80
+ nil
81
+ end
82
+
83
+ private
84
+
85
+ TKIND_COCLASS = 5
86
+ private_constant :TKIND_COCLASS
87
+
88
+ def release_ptr(ptr)
89
+ return if ptr.nil? || ptr.zero?
90
+
91
+ W.vtable_function(ptr, 2, [W::VOIDP], W::DWORD).call(ptr)
92
+ end
93
+
94
+ def get_type_info0(idispatch_ptr)
95
+ out = ("\x00" * W::PTR_SIZE).b
96
+ hr = TI.get_type_info_fn(idispatch_ptr).call(idispatch_ptr, 0, W::LOCALE_SYSTEM_DEFAULT, out)
97
+ return nil if W.failed?(hr)
98
+
99
+ out.unpack1(W::PACK_PTR)
100
+ end
101
+
102
+ def containing_typelib(itypeinfo_ptr)
103
+ tlib_out = ("\x00" * W::PTR_SIZE).b
104
+ index_out = ("\x00" * 4).b
105
+ hr = TI.containing_typelib_fn(itypeinfo_ptr).call(itypeinfo_ptr, tlib_out, index_out)
106
+ return nil if W.failed?(hr)
107
+
108
+ tlib_out.unpack1(W::PACK_PTR)
109
+ end
110
+
111
+ def type_attr_ptr(itypeinfo_ptr)
112
+ out = ("\x00" * W::PTR_SIZE).b
113
+ hr = TI.type_attr_fn(itypeinfo_ptr).call(itypeinfo_ptr, out)
114
+ return nil if W.failed?(hr)
115
+
116
+ out.unpack1(W::PACK_PTR)
117
+ end
118
+
119
+ def type_name(itypeinfo_ptr)
120
+ name_out = ("\x00" * W::PTR_SIZE).b
121
+ TI.documentation_fn_for_typeinfo(itypeinfo_ptr).call(itypeinfo_ptr, -1, name_out, nil, nil, nil)
122
+ bstr = name_out.unpack1(W::PACK_PTR)
123
+ name = W.bstr_to_s(bstr)
124
+ W.sys_free_string.call(bstr) unless bstr.zero?
125
+ name
126
+ end
127
+
128
+ def type_guid(attr_ptr)
129
+ ta = TI::TYPEATTR.new(attr_ptr)
130
+ [ta.guid_Data1, ta.guid_Data2, ta.guid_Data3].pack('LSS') + ta.guid_Data4.pack('C8')
131
+ end
132
+
133
+ def impl_type_ref_typeinfo(itypeinfo_ptr, index)
134
+ href_out = ("\x00" * 4).b
135
+ hr = TI.ref_type_of_impl_type_fn(itypeinfo_ptr).call(itypeinfo_ptr, index, href_out)
136
+ return nil if W.failed?(hr)
137
+
138
+ ref_out = ("\x00" * W::PTR_SIZE).b
139
+ hr = TI.ref_type_info_fn(itypeinfo_ptr).call(itypeinfo_ptr, href_out.unpack1('L'), ref_out)
140
+ return nil if W.failed?(hr)
141
+
142
+ ref_out.unpack1(W::PACK_PTR)
143
+ end
144
+
145
+ # ext/win32ole/win32ole_event.c:481-590 (find_iid, pitf given): scans
146
+ # every COCLASS in ole's containing typelib for an implemented type
147
+ # named itf_name; that impl type's own GUID becomes the event source IID.
148
+ def find_iid_by_name(ole, itf_name)
149
+ itypeinfo_ptr = get_type_info0(ole.dispatch_ptr)
150
+ raise ::RuntimeError, 'interface not found' if itypeinfo_ptr.nil?
151
+
152
+ tlib_ptr = containing_typelib(itypeinfo_ptr)
153
+ release_ptr(itypeinfo_ptr)
154
+ raise ::RuntimeError, 'interface not found' if tlib_ptr.nil?
155
+
156
+ found_iid = nil
157
+ found_ti_ptr = nil
158
+ count = TI.type_info_count_fn(tlib_ptr).call(tlib_ptr)
159
+ count.times do |index|
160
+ break if found_iid
161
+
162
+ ti_out = ("\x00" * W::PTR_SIZE).b
163
+ next if W.failed?(TI.type_info_fn(tlib_ptr).call(tlib_ptr, index, ti_out))
164
+
165
+ ti_ptr = ti_out.unpack1(W::PACK_PTR)
166
+ attr_ptr = type_attr_ptr(ti_ptr)
167
+ if attr_ptr
168
+ if TI::TYPEATTR.new(attr_ptr).typekind == TKIND_COCLASS
169
+ impl_count = TI::TYPEATTR.new(attr_ptr).cImplTypes
170
+ impl_count.times do |t|
171
+ impl_ti_ptr = impl_type_ref_typeinfo(ti_ptr, t)
172
+ next if impl_ti_ptr.nil?
173
+
174
+ if type_name(impl_ti_ptr) == itf_name
175
+ impl_attr_ptr = type_attr_ptr(impl_ti_ptr)
176
+ if impl_attr_ptr
177
+ found_iid = type_guid(impl_attr_ptr)
178
+ TI.release_type_attr_fn(impl_ti_ptr).call(impl_ti_ptr, impl_attr_ptr)
179
+ found_ti_ptr = impl_ti_ptr
180
+ end
181
+ end
182
+ release_ptr(impl_ti_ptr) unless impl_ti_ptr == found_ti_ptr
183
+ break if found_iid
184
+ end
185
+ end
186
+ TI.release_type_attr_fn(ti_ptr).call(ti_ptr, attr_ptr)
187
+ end
188
+ release_ptr(ti_ptr)
189
+ end
190
+ release_ptr(tlib_ptr)
191
+ raise ::RuntimeError, 'interface not found' if found_iid.nil?
192
+
193
+ [found_iid, found_ti_ptr]
194
+ end
195
+
196
+ # ext/win32ole/win32ole_event.c:518-524 (find_iid, pitf NULL): a direct
197
+ # GetTypeInfoOfGuid lookup, used when the caller already knows the IID
198
+ # (the IProvideClassInfo2::GetGUID path in find_default_source).
199
+ def find_iid_by_guid(ole, iid_bytes)
200
+ itypeinfo_ptr = get_type_info0(ole.dispatch_ptr)
201
+ raise ::RuntimeError, 'interface not found' if itypeinfo_ptr.nil?
202
+
203
+ tlib_ptr = containing_typelib(itypeinfo_ptr)
204
+ release_ptr(itypeinfo_ptr)
205
+ raise ::RuntimeError, 'interface not found' if tlib_ptr.nil?
206
+
207
+ ti_out = ("\x00" * W::PTR_SIZE).b
208
+ hr = TI.type_info_of_guid_fn(tlib_ptr).call(tlib_ptr, iid_bytes, ti_out)
209
+ release_ptr(tlib_ptr)
210
+ raise ::RuntimeError, 'interface not found' if W.failed?(hr)
211
+
212
+ ti_out.unpack1(W::PACK_PTR)
213
+ end
214
+
215
+ IMPLTYPEFLAG_FDEFAULT = 0x1
216
+ IMPLTYPEFLAG_FSOURCE = 0x2
217
+ GUIDKIND_DEFAULT_SOURCE_DISP_IID = 1
218
+ private_constant :IMPLTYPEFLAG_FDEFAULT, :IMPLTYPEFLAG_FSOURCE, :GUIDKIND_DEFAULT_SOURCE_DISP_IID
219
+
220
+ def impl_type_flags(itypeinfo_ptr, index)
221
+ flags_out = ("\x00" * 4).b
222
+ hr = TI.impl_type_flags_fn(itypeinfo_ptr).call(itypeinfo_ptr, index, flags_out)
223
+ return nil if W.failed?(hr)
224
+
225
+ flags_out.unpack1('l')
226
+ end
227
+
228
+ # ext/win32ole/win32ole_event.c:667-701
229
+ def find_default_source_from_typeinfo(ti_ptr, attr_ptr)
230
+ count = TI::TYPEATTR.new(attr_ptr).cImplTypes
231
+ count.times do |i|
232
+ flags = impl_type_flags(ti_ptr, i)
233
+ next if flags.nil?
234
+ next if (flags & IMPLTYPEFLAG_FDEFAULT).zero? || (flags & IMPLTYPEFLAG_FSOURCE).zero?
235
+
236
+ ref_ti_ptr = impl_type_ref_typeinfo(ti_ptr, i)
237
+ return ref_ti_ptr if ref_ti_ptr
238
+ end
239
+ nil
240
+ end
241
+
242
+ # ext/win32ole/win32ole_event.c:592-665: find, in ti_ptr's containing
243
+ # typelib, the COCLASS whose default impl type is ti_ptr itself.
244
+ def find_coclass(ti_ptr)
245
+ tlib_ptr = containing_typelib(ti_ptr)
246
+ return [nil, nil] if tlib_ptr.nil?
247
+
248
+ target_attr_ptr = type_attr_ptr(ti_ptr)
249
+ target_guid = target_attr_ptr && type_guid(target_attr_ptr)
250
+ TI.release_type_attr_fn(ti_ptr).call(ti_ptr, target_attr_ptr) if target_attr_ptr
251
+ if target_guid.nil?
252
+ release_ptr(tlib_ptr)
253
+ return [nil, nil]
254
+ end
255
+
256
+ found_ti_ptr = nil
257
+ found_attr_ptr = nil
258
+ count = TI.type_info_count_fn(tlib_ptr).call(tlib_ptr)
259
+ count.times do |i|
260
+ break if found_ti_ptr
261
+
262
+ ti2_out = ("\x00" * W::PTR_SIZE).b
263
+ next if W.failed?(TI.type_info_fn(tlib_ptr).call(tlib_ptr, i, ti2_out))
264
+
265
+ ti2_ptr = ti2_out.unpack1(W::PACK_PTR)
266
+ attr2_ptr = type_attr_ptr(ti2_ptr)
267
+ if attr2_ptr.nil?
268
+ release_ptr(ti2_ptr)
269
+ next
270
+ end
271
+ if TI::TYPEATTR.new(attr2_ptr).typekind != TKIND_COCLASS
272
+ TI.release_type_attr_fn(ti2_ptr).call(ti2_ptr, attr2_ptr)
273
+ release_ptr(ti2_ptr)
274
+ next
275
+ end
276
+
277
+ matched = TI::TYPEATTR.new(attr2_ptr).cImplTypes.times.any? do |j|
278
+ flags = impl_type_flags(ti2_ptr, j)
279
+ next false if flags.nil? || (flags & IMPLTYPEFLAG_FDEFAULT).zero?
280
+
281
+ ref_ti_ptr = impl_type_ref_typeinfo(ti2_ptr, j)
282
+ next false if ref_ti_ptr.nil?
283
+
284
+ ref_attr_ptr = type_attr_ptr(ref_ti_ptr)
285
+ ref_guid = ref_attr_ptr && type_guid(ref_attr_ptr)
286
+ TI.release_type_attr_fn(ref_ti_ptr).call(ref_ti_ptr, ref_attr_ptr) if ref_attr_ptr
287
+ release_ptr(ref_ti_ptr)
288
+ # A candidate whose own TYPEATTR fetch failed has no GUID to
289
+ # compare -- treat it as no match, never as equal to a
290
+ # likewise-missing target GUID (nil == nil would be a false hit).
291
+ !ref_guid.nil? && ref_guid == target_guid
292
+ end
293
+
294
+ if matched
295
+ found_ti_ptr = ti2_ptr
296
+ found_attr_ptr = attr2_ptr
297
+ else
298
+ TI.release_type_attr_fn(ti2_ptr).call(ti2_ptr, attr2_ptr)
299
+ release_ptr(ti2_ptr)
300
+ end
301
+ end
302
+ release_ptr(tlib_ptr)
303
+ [found_ti_ptr, found_attr_ptr]
304
+ end
305
+
306
+ def provide_class_info2_iid(idispatch_ptr)
307
+ pci2_ptr = W.query_interface(idispatch_ptr, W::IID_IPROVIDECLASSINFO2)
308
+ return nil if pci2_ptr.nil?
309
+
310
+ iid_out = ("\x00" * 16).b
311
+ hr = W.vtable_function(pci2_ptr, 4, [W::VOIDP, W::DWORD, W::VOIDP], W::LONG).call(
312
+ pci2_ptr, GUIDKIND_DEFAULT_SOURCE_DISP_IID, iid_out
313
+ )
314
+ release_ptr(pci2_ptr)
315
+ return nil if W.failed?(hr)
316
+
317
+ iid_out
318
+ end
319
+
320
+ def provide_class_info_typeinfo(idispatch_ptr)
321
+ pci_ptr = W.query_interface(idispatch_ptr, W::IID_IPROVIDECLASSINFO)
322
+ return nil if pci_ptr.nil?
323
+
324
+ ti_out = ("\x00" * W::PTR_SIZE).b
325
+ hr = W.vtable_function(pci_ptr, 3, [W::VOIDP, W::VOIDP], W::LONG).call(pci_ptr, ti_out)
326
+ release_ptr(pci_ptr)
327
+ return nil if W.failed?(hr)
328
+
329
+ ti_out.unpack1(W::PACK_PTR)
330
+ end
331
+
332
+ # ext/win32ole/win32ole_event.c:703-786, minus the GetGUID/find_iid
333
+ # fast path (handled by resolve_event_source below, since it needs an
334
+ # `iid_bytes` result rather than an `itypeinfo_ptr` result).
335
+ def find_default_source(ole)
336
+ itypeinfo_ptr = provide_class_info_typeinfo(ole.dispatch_ptr) || get_type_info0(ole.dispatch_ptr)
337
+ raise ::RuntimeError, 'interface not found' if itypeinfo_ptr.nil?
338
+
339
+ attr_ptr = type_attr_ptr(itypeinfo_ptr)
340
+ if attr_ptr.nil?
341
+ release_ptr(itypeinfo_ptr)
342
+ raise ::RuntimeError, 'interface not found'
343
+ end
344
+
345
+ result_ti_ptr = find_default_source_from_typeinfo(itypeinfo_ptr, attr_ptr)
346
+ if result_ti_ptr.nil?
347
+ co_ti_ptr, co_attr_ptr = find_coclass(itypeinfo_ptr)
348
+ if co_ti_ptr
349
+ result_ti_ptr = find_default_source_from_typeinfo(co_ti_ptr, co_attr_ptr)
350
+ TI.release_type_attr_fn(co_ti_ptr).call(co_ti_ptr, co_attr_ptr)
351
+ release_ptr(co_ti_ptr)
352
+ end
353
+ end
354
+ TI.release_type_attr_fn(itypeinfo_ptr).call(itypeinfo_ptr, attr_ptr)
355
+ release_ptr(itypeinfo_ptr)
356
+ raise ::RuntimeError, 'interface not found' if result_ti_ptr.nil?
357
+
358
+ result_attr_ptr = type_attr_ptr(result_ti_ptr)
359
+ if result_attr_ptr.nil?
360
+ release_ptr(result_ti_ptr)
361
+ raise ::RuntimeError, 'interface not found'
362
+ end
363
+ guid = type_guid(result_attr_ptr)
364
+ TI.release_type_attr_fn(result_ti_ptr).call(result_ti_ptr, result_attr_ptr)
365
+ [guid, result_ti_ptr]
366
+ end
367
+
368
+ # The single entry point #advise calls.
369
+ def resolve_event_source(ole, itf)
370
+ return find_iid_by_name(ole, itf) if itf
371
+
372
+ iid_bytes = provide_class_info2_iid(ole.dispatch_ptr)
373
+ if iid_bytes
374
+ begin
375
+ return [iid_bytes, find_iid_by_guid(ole, iid_bytes)]
376
+ rescue ::RuntimeError
377
+ # IProvideClassInfo2 succeeded but the IID it named isn't
378
+ # resolvable in this typelib -- fall through to the
379
+ # IProvideClassInfo/ImplType-traversal path below, matching
380
+ # win32ole_event.c:730-736's own fallthrough.
381
+ end
382
+ end
383
+ find_default_source(ole)
384
+ end
385
+
386
+ # ext/win32ole/win32ole_event.c:900-973 (ev_advise)
387
+ def advise(ole, itf)
388
+ iid_bytes, event_typeinfo_ptr = resolve_event_source(ole, itf)
389
+
390
+ idispatch_ptr = ole.dispatch_ptr
391
+ container_ptr = W.query_interface(idispatch_ptr, W::IID_ICONNECTIONPOINTCONTAINER)
392
+ if container_ptr.nil?
393
+ release_ptr(event_typeinfo_ptr)
394
+ raise WIN32OLE::QueryInterfaceError, W.query_interface_error_message('query IConnectionPointContainer', 'E_NOINTERFACE')
395
+ end
396
+
397
+ cp_out = ("\x00" * W::PTR_SIZE).b
398
+ hr = W.vtable_function(container_ptr, 4, [W::VOIDP, W::VOIDP, W::VOIDP], W::LONG).call(
399
+ container_ptr, iid_bytes, cp_out
400
+ )
401
+ release_ptr(container_ptr)
402
+ if W.failed?(hr)
403
+ release_ptr(event_typeinfo_ptr)
404
+ raise WIN32OLE::QueryInterfaceError, W.query_interface_error_message('query IConnectionPoint', W.hr_hex(hr))
405
+ end
406
+ connection_point_ptr = cp_out.unpack1(W::PACK_PTR)
407
+
408
+ sink_addr, vtable_addr, closures = build_sink(iid_bytes, event_typeinfo_ptr)
409
+ @sink_closures = closures # keep the trampolines alive; see build_sink's own comment
410
+
411
+ cookie_out = ("\x00" * 4).b
412
+ # cookie_out is a DWORD* OUT-parameter (a Ruby byte buffer), so its
413
+ # declared type is VOIDP -- the pointer -- not DWORD, the value it
414
+ # points at.
415
+ hr = W.vtable_function(connection_point_ptr, 5, [W::VOIDP, W::VOIDP, W::VOIDP], W::LONG).call(
416
+ connection_point_ptr, sink_addr, cookie_out
417
+ )
418
+ if W.failed?(hr)
419
+ release_ptr(connection_point_ptr)
420
+ release_ptr(event_typeinfo_ptr)
421
+ Fiddle.free(Fiddle::Pointer.new(sink_addr))
422
+ Fiddle.free(Fiddle::Pointer.new(vtable_addr))
423
+ @sink_closures = nil
424
+ raise WIN32OLE::QueryInterfaceError, W.query_interface_error_message('Advise', W.hr_hex(hr))
425
+ end
426
+
427
+ @event_typeinfo_ptr = event_typeinfo_ptr
428
+ @ole = ole
429
+ # ev_advise's OLE_ADDREF(pDispatch): a Ruby reference alone doesn't
430
+ # order finalizers, so the source's could otherwise run first and
431
+ # destroy the connection point before ours Unadvises it.
432
+ W.add_ref(idispatch_ptr)
433
+ @finalizer_state = {
434
+ cp_ptr: connection_point_ptr, cookie: cookie_out.unpack1('L'), disp_ptr: idispatch_ptr,
435
+ ti_ptr: event_typeinfo_ptr, sink_addr: sink_addr, vtable_addr: vtable_addr,
436
+ # Unadvise calls back into the sink's IUnknown methods. GC frees
437
+ # @sink_closures together with self, before the finalizer runs, so the
438
+ # finalizer must keep these alive itself; they capture no self (see
439
+ # build_sink).
440
+ iunknown_closures: closures.first(3)
441
+ }
442
+ ObjectSpace.define_finalizer(self, self.class.finalizer(@finalizer_state))
443
+ # fev_initialize's evs_push: an advised Event stays reachable until
444
+ # #unadvise, so GC never Unadvises it at an arbitrary point (inside a
445
+ # sink callback, or on another thread).
446
+ ADVISED << self
447
+ end
448
+
449
+ ADVISED = []
450
+ private_constant :ADVISED
451
+
452
+ # Mirrors variant.rb:282-300's discipline exactly: the finalizer proc
453
+ # captures ONLY this data hash, never self and never the Fiddle::Closure
454
+ # objects that capture self (@sink_closures) -- capturing either would
455
+ # create a reference cycle that prevents GC from ever running the
456
+ # finalizer at all.
457
+ # Not deferred like the Release finalizers: as C's dfree does, this
458
+ # Unadvises on whatever thread GC picks (on JRuby, one without OLE).
459
+ def self.finalizer(state)
460
+ proc do
461
+ cp_ptr = state[:cp_ptr]
462
+ if cp_ptr && !cp_ptr.zero?
463
+ W.vtable_function(cp_ptr, 6, [W::VOIDP, W::DWORD], W::LONG).call(cp_ptr, state[:cookie])
464
+ W.vtable_function(cp_ptr, 2, [W::VOIDP], W::DWORD).call(cp_ptr)
465
+ end
466
+ disp_ptr = state[:disp_ptr]
467
+ W.vtable_function(disp_ptr, 2, [W::VOIDP], W::DWORD).call(disp_ptr) if disp_ptr && !disp_ptr.zero?
468
+ ti_ptr = state[:ti_ptr]
469
+ W.vtable_function(ti_ptr, 2, [W::VOIDP], W::DWORD).call(ti_ptr) if ti_ptr && !ti_ptr.zero?
470
+ Fiddle.free(Fiddle::Pointer.new(state[:sink_addr])) if state[:sink_addr]
471
+ Fiddle.free(Fiddle::Pointer.new(state[:vtable_addr])) if state[:vtable_addr]
472
+ end
473
+ end
474
+
475
+ def register_event(event, block, with_outargs)
476
+ if @finalizer_state.nil? || @finalizer_state[:cp_ptr].nil?
477
+ raise WIN32OLE::RuntimeError, 'IConnectionPoint not found. You must call advise at first.'
478
+ end
479
+
480
+ name = event.nil? ? nil : normalize_event_name(event)
481
+ @events.reject! { |e| e[:name] == name }
482
+ @events << { name: name, proc: block, with_outargs: with_outargs }
483
+ nil
484
+ end
485
+
486
+ def normalize_event_name(event)
487
+ unless event.is_a?(String) || event.is_a?(Symbol)
488
+ raise TypeError, 'wrong argument type (expected String or Symbol)'
489
+ end
490
+
491
+ event.to_s
492
+ end
493
+
494
+ SINK_VTBL_SLOTS = 7
495
+ private_constant :SINK_VTBL_SLOTS
496
+
497
+ def self.query_interface_closure(sink_addr, source_iid_bytes, refcount)
498
+ Fiddle::Closure::BlockCaller.new(W::LONG, [W::VOIDP, W::VOIDP, W::VOIDP], W::STDCALL) do |_this, riid_ptr, ppv_ptr|
499
+ riid = riid_ptr[0, 16]
500
+ if riid == W::IID_IUNKNOWN || riid == W::IID_IDISPATCH || riid == source_iid_bytes
501
+ ppv_ptr[0, W::PTR_SIZE] = [sink_addr].pack(W::PACK_PTR)
502
+ refcount[0] += 1
503
+ 0
504
+ else
505
+ ppv_ptr[0, W::PTR_SIZE] = [0].pack(W::PACK_PTR)
506
+ E_NOINTERFACE
507
+ end
508
+ # `rescue Exception`, deliberately, at every closure boundary in this
509
+ # file: the rule is that NO Ruby exception may cross back into the OLE
510
+ # server's native stack frame, and SystemStackError, NoMemoryError,
511
+ # Interrupt and SignalException all descend from Exception rather than
512
+ # StandardError. Letting one of those unwind through a native frame is
513
+ # undefined behaviour, so this is one of the rare places where the
514
+ # broad rescue is the correct choice rather than an anti-pattern.
515
+ rescue Exception => e
516
+ warn_closure_exception('QueryInterface', e)
517
+ E_NOINTERFACE
518
+ end
519
+ end
520
+
521
+ def self.add_ref_closure(refcount)
522
+ Fiddle::Closure::BlockCaller.new(W::DWORD, [W::VOIDP], W::STDCALL) do |_this|
523
+ refcount[0] += 1
524
+ end
525
+ end
526
+
527
+ def self.release_closure(refcount)
528
+ Fiddle::Closure::BlockCaller.new(W::DWORD, [W::VOIDP], W::STDCALL) do |_this|
529
+ refcount[0] -= 1
530
+ refcount[0]
531
+ end
532
+ end
533
+
534
+ def get_type_info_count_closure
535
+ Fiddle::Closure::BlockCaller.new(W::LONG, [W::VOIDP, W::VOIDP], W::STDCALL) do |_this, pct_ptr|
536
+ pct_ptr[0, 4] = [0].pack('L')
537
+ 0
538
+ end
539
+ end
540
+
541
+ def get_type_info_closure
542
+ Fiddle::Closure::BlockCaller.new(W::LONG, [W::VOIDP, W::DWORD, W::DWORD, W::VOIDP], W::STDCALL) do |_this, _idx, _lcid, ppti_ptr|
543
+ ppti_ptr[0, W::PTR_SIZE] = [0].pack(W::PACK_PTR)
544
+ DISP_E_BADINDEX
545
+ end
546
+ end
547
+
548
+ def get_ids_of_names_closure(event_typeinfo_ptr)
549
+ Fiddle::Closure::BlockCaller.new(
550
+ W::LONG, [W::VOIDP, W::VOIDP, W::VOIDP, W::DWORD, W::DWORD, W::VOIDP], W::STDCALL
551
+ ) do |_this, _riid, names_ptr, cnames, _lcid, dispids_ptr|
552
+ TI.get_ids_of_names_fn(event_typeinfo_ptr).call(event_typeinfo_ptr, names_ptr.to_i, cnames, dispids_ptr.to_i)
553
+ rescue Exception => e
554
+ warn_closure_exception('GetIDsOfNames', e)
555
+ DISP_E_UNKNOWNNAME
556
+ end
557
+ end
558
+
559
+ DISP_E_UNKNOWNNAME = -2147352570
560
+ E_NOINTERFACE = -2147467262
561
+ DISP_E_BADINDEX = -2147352565
562
+ private_constant :DISP_E_UNKNOWNNAME, :E_NOINTERFACE, :DISP_E_BADINDEX
563
+
564
+ def warn_closure_exception(where, error)
565
+ self.class.warn_closure_exception(where, error)
566
+ end
567
+
568
+ def self.warn_closure_exception(where, error)
569
+ backtrace = error.backtrace || []
570
+ warn "#{backtrace.first}: #{error.message} (#{error.class}) in WIN32OLE::Event sink's #{where}"
571
+ backtrace.drop(1).each { |line| warn "\tfrom #{line}" }
572
+ end
573
+
574
+ # The event ITypeInfo* this sink resolves names against is read from
575
+ # @event_typeinfo_ptr (set once by #advise), not captured here -- the
576
+ # parameter is kept only so build_sink calls every closure builder the
577
+ # same way.
578
+ def invoke_closure(_event_typeinfo_ptr)
579
+ Fiddle::Closure::BlockCaller.new(
580
+ W::LONG, [W::VOIDP, W::LONG, W::VOIDP, W::DWORD, W::WORD, W::VOIDP, W::VOIDP, W::VOIDP, W::VOIDP], W::STDCALL
581
+ ) do |_this, dispid, _riid, _lcid, _wflags, pdispparams_ptr, pvarresult_ptr, _pexcepinfo_ptr, _puargerr_ptr|
582
+ handle_invoke(dispid, pdispparams_ptr, pvarresult_ptr)
583
+ 0 # NOERROR, always -- see Global Constraints: no exception may cross this boundary.
584
+ rescue Exception => e
585
+ warn_closure_exception('Invoke', e)
586
+ 0
587
+ end
588
+ end
589
+
590
+ def resolve_event_name(dispid)
591
+ bstr_out = ("\x00" * W::PTR_SIZE).b
592
+ count_out = ("\x00" * 4).b
593
+ hr = TI.get_names_fn(@event_typeinfo_ptr).call(@event_typeinfo_ptr, dispid, bstr_out, 1, count_out)
594
+ return nil if W.failed?(hr)
595
+
596
+ bstr = bstr_out.unpack1(W::PACK_PTR)
597
+ name = W.bstr_to_s(bstr)
598
+ W.sys_free_string.call(bstr) unless bstr.zero?
599
+ name
600
+ end
601
+
602
+ # ext/win32ole/win32ole_event.c:813-836 (ole_search_event): a NAMED
603
+ # match wins immediately; otherwise fall back to the one catch-all
604
+ # (nil-name) entry, if any. Returns [entry_or_nil, is_default] --
605
+ # is_default is true only when a catch-all entry was actually found
606
+ # (matching the C source, which leaves *is_default FALSE when the
607
+ # array has no catch-all at all -- drives whether the event name gets
608
+ # prepended to the callback's args).
609
+ def find_event_entry(name)
610
+ fallback = nil
611
+ is_default = false
612
+ @events.each do |e|
613
+ return [e, false] if e[:name] == name
614
+
615
+ if e[:name].nil?
616
+ fallback = e
617
+ is_default = true
618
+ end
619
+ end
620
+ [fallback, is_default]
621
+ end
622
+
623
+ def read_dispparams(pdispparams_ptr)
624
+ rgvarg_addr = pdispparams_ptr[0, W::PTR_SIZE].unpack1(W::PACK_PTR)
625
+ cargs = pdispparams_ptr[2 * W::PTR_SIZE, 4].unpack1('L')
626
+ [cargs, rgvarg_addr]
627
+ end
628
+
629
+ # ext/win32ole/win32ole_event.c:132-234 (EVENTSINK_Invoke), including
630
+ # the Hash/Array out-argument write-back (#write_hash_result,
631
+ # #write_array_outargs).
632
+ def handle_invoke(dispid, pdispparams_ptr, pvarresult_ptr)
633
+ name = resolve_event_name(dispid)
634
+ return if name.nil?
635
+
636
+ entry, is_default = find_event_entry(name)
637
+ handler_obj = nil
638
+ mid = nil
639
+ with_outargs = false
640
+ if entry
641
+ handler_obj = entry[:proc]
642
+ mid = :call
643
+ with_outargs = entry[:with_outargs]
644
+ elsif @handler
645
+ on_name = "on#{name}"
646
+ if @handler.respond_to?(on_name)
647
+ handler_obj = @handler
648
+ mid = on_name
649
+ is_default = false
650
+ elsif @handler.respond_to?(:method_missing)
651
+ handler_obj = @handler
652
+ mid = :method_missing
653
+ is_default = true
654
+ end
655
+ end
656
+ return if handler_obj.nil? || mid.nil?
657
+
658
+ args = []
659
+ args << name if is_default
660
+ cargs, rgvarg_addr = read_dispparams(pdispparams_ptr)
661
+ cargs.times do |i|
662
+ var_ptr = Fiddle::Pointer.new(rgvarg_addr + (cargs - i - 1) * W::VARIANT_SIZE)
663
+ # rgvarg belongs to the event source; read it without consuming it
664
+ args << WIN32OLE.variant_bytes_to_ruby_value(var_ptr[0, W::VARIANT_SIZE], owned: false)
665
+ end
666
+ outargv = nil
667
+ if with_outargs
668
+ outargv = []
669
+ args << outargv
670
+ end
671
+
672
+ result = begin
673
+ handler_obj.send(mid, *args)
674
+ rescue Exception => e # see query_interface_closure: still inside the closure's native frame
675
+ warn_closure_exception('an event callback', e)
676
+ nil
677
+ end
678
+
679
+ if result.is_a?(Hash)
680
+ write_hash_result(result, dispid, cargs, rgvarg_addr)
681
+ # key? rather than `||`, so a legitimate {'return' => false} yields
682
+ # false instead of falling through to the symbol key.
683
+ result = result.key?('return') ? result['return'] : result[:return]
684
+ elsif with_outargs && outargv.is_a?(Array)
685
+ write_array_outargs(outargv, cargs, rgvarg_addr)
686
+ end
687
+
688
+ return if pvarresult_ptr.nil? || pvarresult_ptr.to_i.zero?
689
+
690
+ bytes = begin
691
+ WIN32OLE.ruby_value_to_variant_bytes(result, [])
692
+ rescue StandardError
693
+ W.pack_variant(W::VT_EMPTY, W.pack_empty)
694
+ end
695
+ pvarresult_ptr[0, W::VARIANT_SIZE] = bytes
696
+ end
697
+
698
+ # ext/win32ole/win32ole_event.c:333-399 (ole_val2ptr_variant), ported
699
+ # 1:1 including its "silently do nothing for an unhandled
700
+ # type/VARTYPE combination" fallthrough -- e.g. writing a String into a
701
+ # VT_I4|BYREF slot is a deliberate no-op in the original, not a bug we
702
+ # should "fix" by raising.
703
+ def write_byref_variant(var_ptr, value)
704
+ vt = var_ptr[0, 2].unpack1('S')
705
+ return if (vt & W::VT_BYREF).zero?
706
+
707
+ ref_addr = var_ptr[8, W::PTR_SIZE].unpack1(W::PACK_PTR)
708
+ return if ref_addr.zero?
709
+
710
+ ref_ptr = Fiddle::Pointer.new(ref_addr)
711
+ base_vt = vt & ~W::VT_BYREF
712
+ case value
713
+ when String
714
+ ref_ptr[0, W::PTR_SIZE] = [W.sys_alloc_string.call(W.wstr(value))].pack(W::PACK_PTR) if base_vt == W::VT_BSTR
715
+ when Integer
716
+ case base_vt
717
+ when W::VT_UI1 then ref_ptr[0, 1] = [value].pack('C')
718
+ when W::VT_I2 then ref_ptr[0, 2] = [value].pack('s')
719
+ when W::VT_I4 then ref_ptr[0, 4] = [value].pack('l')
720
+ when W::VT_R4 then ref_ptr[0, 4] = [value.to_f].pack('f')
721
+ when W::VT_R8 then ref_ptr[0, 8] = [value.to_f].pack('d')
722
+ end
723
+ when Float
724
+ case base_vt
725
+ when W::VT_I2 then ref_ptr[0, 2] = [value.to_i].pack('s')
726
+ when W::VT_I4 then ref_ptr[0, 4] = [value.to_i].pack('l')
727
+ when W::VT_R4 then ref_ptr[0, 4] = [value].pack('f')
728
+ when W::VT_R8 then ref_ptr[0, 8] = [value].pack('d')
729
+ end
730
+ when true, false
731
+ ref_ptr[0, 2] = [value ? -1 : 0].pack('s') if base_vt == W::VT_BOOL
732
+ end
733
+ end
734
+
735
+ def write_array_outargs(ary, cargs, rgvarg_addr)
736
+ ary.each_with_index do |value, i|
737
+ break if i >= cargs
738
+
739
+ var_ptr = Fiddle::Pointer.new(rgvarg_addr + (cargs - i - 1) * W::VARIANT_SIZE)
740
+ write_byref_variant(var_ptr, value)
741
+ end
742
+ end
743
+
744
+ # ext/win32ole/win32ole_event.c:401-428 (hash2ptr_dispparams)
745
+ def write_hash_result(hash, dispid, cargs, rgvarg_addr)
746
+ names_out = Fiddle::Pointer.malloc(W::PTR_SIZE * (cargs + 1))
747
+ count_out = ("\x00" * 4).b
748
+ hr = TI.get_names_fn(@event_typeinfo_ptr).call(@event_typeinfo_ptr, dispid, names_out, cargs + 1, count_out)
749
+ return if W.failed?(hr)
750
+
751
+ len = count_out.unpack1('L')
752
+ (len - 1).times do |i|
753
+ bstr = names_out[(i + 1) * W::PTR_SIZE, W::PTR_SIZE].unpack1(W::PACK_PTR)
754
+ key_name = W.bstr_to_s(bstr)
755
+ W.sys_free_string.call(bstr) unless bstr.zero?
756
+
757
+ value = hash[i]
758
+ value = hash[key_name] if value.nil?
759
+ value = hash[key_name.to_sym] if value.nil? && key_name
760
+
761
+ var_ptr = Fiddle::Pointer.new(rgvarg_addr + (cargs - i - 1) * W::VARIANT_SIZE)
762
+ write_byref_variant(var_ptr, value)
763
+ end
764
+ ensure
765
+ Fiddle.free(names_out) if names_out
766
+ end
767
+
768
+ # Builds a fresh 7-slot IDispatch-shaped vtable (QueryInterface, AddRef,
769
+ # Release, GetTypeInfoCount, GetTypeInfo, GetIDsOfNames, Invoke -- same
770
+ # order/shape as ext/win32ole/win32ole_event.c's IEventSinkVtbl) backed
771
+ # by Fiddle::Closure::BlockCaller trampolines. Returns raw addresses (not
772
+ # Fiddle::Pointer wrappers) plus the closures themselves -- the CALLER
773
+ # must keep `closures` referenced for as long as the sink is advised
774
+ # (GC'ing a Closure frees its native trampoline), matching variant.rb's
775
+ # own finalizer-state discipline (see #advise).
776
+ def build_sink(source_iid_bytes, event_typeinfo_ptr)
777
+ sink_ptr = Fiddle::Pointer.malloc(W::PTR_SIZE)
778
+ sink_addr = sink_ptr.to_i
779
+ refcount = [0]
780
+
781
+ closures = [
782
+ # Class methods, so these blocks capture the class rather than self
783
+ # and #advise can hand them to the finalizer.
784
+ self.class.query_interface_closure(sink_addr, source_iid_bytes, refcount),
785
+ self.class.add_ref_closure(refcount),
786
+ self.class.release_closure(refcount),
787
+ get_type_info_count_closure,
788
+ get_type_info_closure,
789
+ get_ids_of_names_closure(event_typeinfo_ptr),
790
+ invoke_closure(event_typeinfo_ptr)
791
+ ]
792
+
793
+ vtable_ptr = Fiddle::Pointer.malloc(W::PTR_SIZE * SINK_VTBL_SLOTS)
794
+ closures.each_with_index { |c, i| vtable_ptr[i * W::PTR_SIZE, W::PTR_SIZE] = [c.to_i].pack(W::PACK_PTR) }
795
+ sink_ptr[0, W::PTR_SIZE] = [vtable_ptr.to_i].pack(W::PACK_PTR)
796
+
797
+ [sink_addr, vtable_ptr.to_i, closures]
798
+ end
799
+ end
800
+ end