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,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fiddle'
4
+
5
+ # Backports, for JRuby's older Fiddle FFI backends, behaviours this
6
+ # implementation relies on and newer fiddle already has. JRuby 9.4 ships
7
+ # fiddle 1.1.4, 10.0 ships 1.1.6, and 10.1 ships 1.1.8, which needs none of
8
+ # this. CRuby's C fiddle has always behaved this way.
9
+ if RUBY_ENGINE == 'jruby'
10
+ class WIN32OLE
11
+ module FiddleCompat
12
+ FIDDLE_VERSION = Gem::Version.new(Fiddle::VERSION)
13
+
14
+ # fiddle 1.1.6: an Integer address passed for a VOIDP argument.
15
+ module IntegerVoidp
16
+ def call(*args, &block)
17
+ @args.each_with_index do |type, i|
18
+ args[i] = Fiddle::Pointer.new(args[i]) if type == Fiddle::TYPE_VOIDP && args[i].is_a?(Integer)
19
+ end
20
+ super
21
+ end
22
+ end
23
+
24
+ # fiddle 1.1.8: a closure's pointer arguments as Fiddle::Pointer, not FFI::Pointer.
25
+ module FiddlePointerArgs
26
+ def call(*args)
27
+ super(*args.map { |v| v.is_a?(FFI::Pointer) ? Fiddle::Pointer.new(v) : v })
28
+ end
29
+ end
30
+
31
+ # fiddle 1.1.8: Pointer.to_ptr(a_string) with room for the NUL written
32
+ # after the bytes (older ones malloc only bytesize, overrunning the heap).
33
+ module StringToPtr
34
+ def to_ptr(value)
35
+ return super unless value.is_a?(String)
36
+
37
+ new(FFI::MemoryPointer.from_string(value), value.bytesize)
38
+ end
39
+ alias [] to_ptr
40
+ end
41
+
42
+ Fiddle::Function.prepend(IntegerVoidp) if FIDDLE_VERSION < Gem::Version.new('1.1.6')
43
+ if FIDDLE_VERSION < Gem::Version.new('1.1.8')
44
+ Fiddle::Closure::BlockCaller.prepend(FiddlePointerArgs)
45
+ Fiddle::Pointer.singleton_class.prepend(StringToPtr)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'win32ole/ffi/win32'
4
+ require 'win32ole/ffi/typeinfo'
5
+ require 'win32ole/ffi/param'
6
+
7
+ class WIN32OLE
8
+ class Method
9
+ W = Win32
10
+ TI = TypeInfo
11
+ private_constant :W, :TI
12
+
13
+ FUNCFLAG_FRESTRICTED = 0x1
14
+ FUNCFLAG_FHIDDEN = 0x40 # per MEMBERID/FUNCFLAGS, distinct from TYPEFLAG's own 0x10
15
+ FUNCFLAG_FNONBROWSABLE = 0x400
16
+
17
+ # Mirrors ext/win32ole/win32ole_method.c's folemethod_initialize:
18
+ # WIN32OLE::Method.new(oletype, method_name) searches oletype's own
19
+ # funcs first, then (since a coclass like "Shell" has none of its own
20
+ # -- its methods live on the interfaces it implements) one level of
21
+ # its implemented types, case-insensitively by name (COM method names
22
+ # are case-insensitive).
23
+ def self.new(oletype, method)
24
+ raise TypeError, '1st argument should be WIN32OLE::Type object' unless oletype.is_a?(WIN32OLE::Type)
25
+
26
+ method = check_string!(method)
27
+ owner_ptr = oletype.itypeinfo_ptr
28
+ index = func_index_in(owner_ptr, method)
29
+ return from_typeinfo_ptr(owner_ptr, index) if index
30
+
31
+ # ole_method_sub(self, pTypeInfo, pRefTypeInfo, ...): a match on an
32
+ # implemented type records oletype as the owner (for #event?).
33
+ # initialize only reads ref_ptr synchronously, so it is released here
34
+ # either way.
35
+ TI.each_impl_type(owner_ptr) do |_flags, ref_ptr|
36
+ begin
37
+ ref_index = func_index_in(ref_ptr, method)
38
+ return from_typeinfo_ptr(ref_ptr, ref_index, owner_ptr) if ref_index
39
+ ensure
40
+ TI.release(ref_ptr)
41
+ end
42
+ end
43
+ raise WIN32OLE::RuntimeError, "not found #{method}"
44
+ end
45
+
46
+ def self.check_string!(val)
47
+ return val if val.is_a?(::String)
48
+ return val.to_str if val.respond_to?(:to_str)
49
+
50
+ raise TypeError, "no implicit conversion of #{val.class} into String"
51
+ end
52
+ private_class_method :check_string!
53
+
54
+ # Index of the FUNCDESC in itypeinfo_ptr whose name matches
55
+ # case-insensitively, or nil.
56
+ def self.func_index_in(itypeinfo_ptr, name)
57
+ (TI.type_attr_value(itypeinfo_ptr, :cFuncs) || 0).times do |i|
58
+ funcdesc_out = ("\x00" * W::PTR_SIZE).b
59
+ next if W.failed?(TI.func_desc_fn(itypeinfo_ptr).call(itypeinfo_ptr, i, funcdesc_out))
60
+
61
+ funcdesc_ptr = funcdesc_out.unpack1(W::PACK_PTR)
62
+ memid = TI::FUNCDESC.new(funcdesc_ptr).memid
63
+ TI.release_func_desc_fn(itypeinfo_ptr).call(itypeinfo_ptr, funcdesc_ptr)
64
+
65
+ fname = TI.member_name(itypeinfo_ptr, memid)
66
+ return i if fname && fname.casecmp?(name)
67
+ end
68
+ nil
69
+ end
70
+ private_class_method :func_index_in
71
+
72
+ # owner_ptr is the coclass whose implemented type itypeinfo_ptr is
73
+ # (olemethoddata's pOwnerTypeInfo), or nil for a type's own function.
74
+ def self.from_typeinfo_ptr(itypeinfo_ptr, index, owner_ptr = nil)
75
+ allocate.tap { |method| method.send(:initialize, itypeinfo_ptr, index, owner_ptr) }
76
+ end
77
+
78
+ def initialize(itypeinfo_ptr, index, owner_ptr = nil)
79
+ funcdesc_out = ("\x00" * W::PTR_SIZE).b
80
+ hr = TI.func_desc_fn(itypeinfo_ptr).call(itypeinfo_ptr, index, funcdesc_out)
81
+ if W.failed?(hr)
82
+ raise WIN32OLE::QueryInterfaceError, W.query_interface_error_message('GetFuncDesc', W.hr_hex(hr))
83
+ end
84
+ funcdesc_ptr = funcdesc_out.unpack1(W::PACK_PTR)
85
+ funcdesc = TI::FUNCDESC.new(funcdesc_ptr)
86
+
87
+ @memid = funcdesc.memid
88
+ @invkind = funcdesc.invkind
89
+ @dispid = funcdesc.memid
90
+ @offset_vtbl = funcdesc.oVft
91
+ @size_params = funcdesc.cParams
92
+ @size_opt_params = funcdesc.cParamsOpt
93
+ @func_flags = funcdesc.wFuncFlags
94
+ @return_vt = funcdesc.ret_tdesc_vt
95
+ @return_type_detail = TI.typedesc_details(
96
+ itypeinfo_ptr, funcdesc_ptr + TI.offsetof(TI::FUNCDESC, 'ret_tdesc_union_ptr')
97
+ )
98
+
99
+ @name, param_names = read_names(itypeinfo_ptr, @memid, funcdesc.cParams)
100
+ # folemethod_event / folemethod_event_interface, computed now because
101
+ # neither ITypeInfo* is retained past construction.
102
+ @event = owner_ptr ? TI.method_event?(owner_ptr, index, @name) : false
103
+ @event_interface = @event ? TI.type_name(itypeinfo_ptr) : nil
104
+ @helpstring, @help_context, @helpfile = read_documentation(itypeinfo_ptr, @memid)
105
+
106
+ # lprgelemdescParam is a POINTER FIELD inside FUNCDESC — its value is
107
+ # the address of a contiguous array of cParams ELEMDESC structs, not
108
+ # an offset into FUNCDESC itself.
109
+ elemdesc_array_ptr = funcdesc.lprgelemdescParam
110
+ # MRI builds one Param per name GetNames actually returned beyond the
111
+ # member's own name (param_names.length), not per FUNCDESC.cParams —
112
+ # for a property-put method, GetNames returns only the property name
113
+ # (cParams=1 but zero param names), so cParams would otherwise build a
114
+ # spurious nameless Param.
115
+ @params = Array.new(param_names.length) do |i|
116
+ elemdesc_ptr = elemdesc_array_ptr + i * TI::ELEMDESC.size
117
+ WIN32OLE::Param.from_elemdesc(itypeinfo_ptr, elemdesc_ptr, param_names[i])
118
+ end
119
+
120
+ TI.release_func_desc_fn(itypeinfo_ptr).call(itypeinfo_ptr, funcdesc_ptr)
121
+ end
122
+
123
+ def name
124
+ @name
125
+ end
126
+ alias to_s name
127
+
128
+ def return_type
129
+ @return_type_detail.last
130
+ end
131
+
132
+ def return_vtype
133
+ @return_vt
134
+ end
135
+
136
+ def return_type_detail
137
+ @return_type_detail.dup
138
+ end
139
+
140
+ def invoke_kind
141
+ TI.invoke_kind_name(@invkind)
142
+ end
143
+
144
+ def invkind
145
+ @invkind
146
+ end
147
+
148
+ def visible?
149
+ (@func_flags & (FUNCFLAG_FRESTRICTED | FUNCFLAG_FHIDDEN | FUNCFLAG_FNONBROWSABLE)) == 0
150
+ end
151
+
152
+ def dispid
153
+ @dispid
154
+ end
155
+
156
+ def helpstring
157
+ @helpstring
158
+ end
159
+
160
+ def helpfile
161
+ @helpfile
162
+ end
163
+
164
+ def helpcontext
165
+ @help_context
166
+ end
167
+
168
+ def offset_vtbl
169
+ @offset_vtbl
170
+ end
171
+
172
+ def size_params
173
+ @size_params
174
+ end
175
+
176
+ def size_opt_params
177
+ @size_opt_params
178
+ end
179
+
180
+ def params
181
+ @params
182
+ end
183
+
184
+ def inspect
185
+ "#<WIN32OLE::Method:#{name}>"
186
+ end
187
+
188
+ def event?
189
+ @event
190
+ end
191
+
192
+ def event_interface
193
+ @event_interface
194
+ end
195
+
196
+ private
197
+
198
+ def read_names(itypeinfo_ptr, memid, cparams)
199
+ # GetNames(MEMBERID memid, BSTR *rgBstrNames, UINT cMaxNames, UINT *pcNames)
200
+ # rgBstrNames[0] is the member's own name; rgBstrNames[1..] are param names.
201
+ max_names = cparams + 1
202
+ names_out = ("\x00" * (max_names * W::PTR_SIZE)).b
203
+ count_out = ("\x00" * 4).b
204
+ get_names_fn(itypeinfo_ptr).call(itypeinfo_ptr, memid, names_out, max_names, count_out)
205
+ count = count_out.unpack1('L')
206
+ bstrs = names_out.unpack(W::PACK_PTR * count)
207
+ strings = bstrs.map { |b| W.bstr_to_s(b) }
208
+ bstrs.each { |b| W.sys_free_string.call(b) unless b.zero? }
209
+ [strings[0], strings[1..] || []]
210
+ end
211
+
212
+ def get_names_fn(itypeinfo_ptr)
213
+ @@get_names_fns ||= {}
214
+ @@get_names_fns[W.vtable_address(itypeinfo_ptr)] ||= W.vtable_function(
215
+ itypeinfo_ptr, TI::ITYPEINFO_VTBL[:GetNames],
216
+ [W::VOIDP, W::LONG, W::VOIDP, W::DWORD, W::VOIDP], W::LONG
217
+ )
218
+ end
219
+
220
+ def read_documentation(itypeinfo_ptr, memid)
221
+ name_out = ("\x00" * W::PTR_SIZE).b
222
+ docstring_out = ("\x00" * W::PTR_SIZE).b
223
+ helpcontext_out = ("\x00" * 4).b
224
+ helpfile_out = ("\x00" * W::PTR_SIZE).b
225
+ TI.documentation_fn_for_typeinfo(itypeinfo_ptr).call(
226
+ itypeinfo_ptr, memid, name_out, docstring_out, helpcontext_out, helpfile_out
227
+ )
228
+ name_bstr = name_out.unpack1(W::PACK_PTR)
229
+ docstring_bstr = docstring_out.unpack1(W::PACK_PTR)
230
+ helpfile_bstr = helpfile_out.unpack1(W::PACK_PTR)
231
+ helpstring = W.bstr_to_s(docstring_bstr) || '' # C's WC2VSTR(NULL) is '', not nil
232
+ helpfile = W.bstr_to_s(helpfile_bstr) || ''
233
+ [name_bstr, docstring_bstr, helpfile_bstr].each { |b| W.sys_free_string.call(b) unless b.zero? }
234
+ [helpstring, helpcontext_out.unpack1('L'), helpfile]
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'win32ole/ffi/win32'
4
+ require 'win32ole/ffi/typeinfo'
5
+
6
+ class WIN32OLE
7
+ class Param
8
+ W = Win32
9
+ TI = TypeInfo
10
+ private_constant :W, :TI
11
+
12
+ PARAMFLAG_FIN = 0x1
13
+ PARAMFLAG_FOUT = 0x2
14
+ PARAMFLAG_FRETVAL = 0x8
15
+ PARAMFLAG_FOPT = 0x10
16
+ PARAMFLAG_FHASDEFAULT = 0x20
17
+ PARAMDESCEX_DEFAULT_VALUE_OFFSET = 8
18
+
19
+ # ext/win32ole/win32ole_param.c's foleparam_initialize: the n-th
20
+ # (1-based) parameter of a WIN32OLE::Method.
21
+ def self.new(olemethod, n)
22
+ raise TypeError, '1st parameter must be WIN32OLE::Method object' unless olemethod.is_a?(WIN32OLE::Method)
23
+ raise TypeError, "no implicit conversion of #{n.class} into Integer" unless n.is_a?(::Integer)
24
+
25
+ params = olemethod.params
26
+ raise IndexError, "index of param must be in 1..#{params.size}" unless (1..params.size).cover?(n)
27
+
28
+ params[n - 1]
29
+ end
30
+
31
+ def self.from_elemdesc(itypeinfo_ptr, elemdesc_ptr, name)
32
+ allocate.tap { |param| param.send(:initialize, itypeinfo_ptr, elemdesc_ptr, name) }
33
+ end
34
+
35
+ def initialize(itypeinfo_ptr, elemdesc_ptr, name)
36
+ @name = name
37
+ elemdesc = TI::ELEMDESC.new(elemdesc_ptr)
38
+ @type_detail = TI.typedesc_details(
39
+ itypeinfo_ptr, elemdesc_ptr.to_i + TI.offsetof(TI::ELEMDESC, 'tdesc_union_ptr')
40
+ )
41
+ @param_flags = elemdesc.wParamFlags
42
+ # Read the default value now, not lazily: PARAMDESCEX lives inside the
43
+ # owning Method's FUNCDESC, which the Method releases (ReleaseFuncDesc)
44
+ # right after all of its Params are constructed. Holding the raw
45
+ # pointer past this point and dereferencing it later (e.g. from a
46
+ # public #default call) would be a use-after-free.
47
+ @default = read_default(elemdesc.paramdescex_ptr)
48
+ end
49
+
50
+ def name
51
+ @name
52
+ end
53
+ alias to_s name
54
+
55
+ def ole_type
56
+ @type_detail.last
57
+ end
58
+
59
+ def ole_type_detail
60
+ @type_detail.dup
61
+ end
62
+
63
+ def input?
64
+ (@param_flags & PARAMFLAG_FIN) != 0
65
+ end
66
+
67
+ def output?
68
+ (@param_flags & PARAMFLAG_FOUT) != 0
69
+ end
70
+
71
+ def optional?
72
+ (@param_flags & PARAMFLAG_FOPT) != 0
73
+ end
74
+
75
+ def retval?
76
+ (@param_flags & PARAMFLAG_FRETVAL) != 0
77
+ end
78
+
79
+ def default
80
+ @default
81
+ end
82
+
83
+ # ext/win32ole/win32ole_param.c's foleparam_inspect: the default value,
84
+ # not the type, follows the name -- and only when there is one.
85
+ def inspect
86
+ detail = @default.nil? ? name : "#{name}=#{@default.inspect}"
87
+ "#<WIN32OLE::Param:#{detail}>"
88
+ end
89
+
90
+ private
91
+
92
+ def read_default(paramdescex_ptr)
93
+ return nil unless (@param_flags & PARAMFLAG_FHASDEFAULT) != 0
94
+ return nil if paramdescex_ptr.to_i.zero?
95
+
96
+ # PARAMDESCEX is { ULONG cBytes; VARIANTARG varDefaultValue; } -- a
97
+ # VARIANT holds a DOUBLE/LONGLONG, so it is 8-byte aligned and starts
98
+ # at offset 8 (cBytes plus 4 bytes of padding), not right after cBytes.
99
+ variant_bytes = Fiddle::Pointer.new(paramdescex_ptr)[PARAMDESCEX_DEFAULT_VALUE_OFFSET, W::VARIANT_SIZE]
100
+ vt, payload = W.unpack_variant(variant_bytes)
101
+ case W.variant_ruby_type(vt)
102
+ when :i4 then W.unpack_i4(payload)
103
+ when :i8 then W.unpack_i8(payload)
104
+ when :r8 then W.unpack_r8(payload)
105
+ when :bool then W.unpack_bool(payload)
106
+ when :bstr then W.bstr_to_s(W.unpack_pointer(payload))
107
+ else nil
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,289 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fiddle'
4
+ require 'win32ole/ffi/win32'
5
+ require 'win32ole/ffi/typeinfo'
6
+
7
+ class WIN32OLE
8
+ class Record
9
+ W = Win32
10
+ TI = TypeInfo
11
+ private_constant :W, :TI
12
+
13
+ # Full slot table transcribed from oaidl.h, kept for documentation
14
+ # completeness (only the six below have a Fiddle::Function bound; the
15
+ # rest are unused).
16
+ IRECORDINFO_VTBL = {
17
+ RecordInit: 3, RecordClear: 4, RecordCopy: 5, GetGuid: 6, GetName: 7,
18
+ GetSize: 8, GetTypeInfo: 9, GetField: 10, GetFieldNoCopy: 11,
19
+ PutField: 12, PutFieldNoCopy: 13, GetFieldNames: 14, IsMatchingType: 15,
20
+ RecordCreate: 16, RecordCreateCopy: 17, RecordDestroy: 18
21
+ }.freeze
22
+
23
+ def self.record_init_fn(pri)
24
+ @record_init_fns ||= {}
25
+ @record_init_fns[W.vtable_address(pri)] ||= W.vtable_function(
26
+ pri, IRECORDINFO_VTBL[:RecordInit], [W::VOIDP, W::VOIDP], W::LONG
27
+ )
28
+ end
29
+
30
+ def self.get_name_fn(pri)
31
+ @get_name_fns ||= {}
32
+ @get_name_fns[W.vtable_address(pri)] ||= W.vtable_function(
33
+ pri, IRECORDINFO_VTBL[:GetName], [W::VOIDP, W::VOIDP], W::LONG
34
+ )
35
+ end
36
+
37
+ def self.get_size_fn(pri)
38
+ @get_size_fns ||= {}
39
+ @get_size_fns[W.vtable_address(pri)] ||= W.vtable_function(
40
+ pri, IRECORDINFO_VTBL[:GetSize], [W::VOIDP, W::VOIDP], W::LONG
41
+ )
42
+ end
43
+
44
+ def self.get_field_no_copy_fn(pri)
45
+ @get_field_no_copy_fns ||= {}
46
+ @get_field_no_copy_fns[W.vtable_address(pri)] ||= W.vtable_function(
47
+ pri, IRECORDINFO_VTBL[:GetFieldNoCopy], [W::VOIDP, W::VOIDP, W::VOIDP, W::VOIDP, W::VOIDP], W::LONG
48
+ )
49
+ end
50
+
51
+ def self.put_field_fn(pri)
52
+ @put_field_fns ||= {}
53
+ @put_field_fns[W.vtable_address(pri)] ||= W.vtable_function(
54
+ pri, IRECORDINFO_VTBL[:PutField], [W::VOIDP, W::DWORD, W::VOIDP, W::VOIDP, W::VOIDP], W::LONG
55
+ )
56
+ end
57
+
58
+ def self.get_field_names_fn(pri)
59
+ @get_field_names_fns ||= {}
60
+ @get_field_names_fns[W.vtable_address(pri)] ||= W.vtable_function(
61
+ pri, IRECORDINFO_VTBL[:GetFieldNames], [W::VOIDP, W::VOIDP, W::VOIDP], W::LONG
62
+ )
63
+ end
64
+
65
+ def self.get_record_info_from_type_info_fn
66
+ @get_record_info_from_type_info_fn ||= Fiddle::Function.new(
67
+ W.oleaut32['GetRecordInfoFromTypeInfo'], [W::VOIDP, W::VOIDP], W::LONG, W::STDCALL
68
+ )
69
+ end
70
+
71
+ def initialize(typename, oleobj)
72
+ typename = typename.to_s
73
+ itypelib_ptr = resolve_itypelib_ptr(oleobj)
74
+ pri = find_record_info(itypelib_ptr, typename)
75
+ unless pri
76
+ raise WIN32OLE::RuntimeError, "fail to query IRecordInfo interface for `#{typename}'"
77
+ end
78
+
79
+ set_record_info(pri, nil)
80
+ end
81
+
82
+ def to_h
83
+ @fields
84
+ end
85
+
86
+ def typename
87
+ @typename
88
+ end
89
+
90
+ def inspect
91
+ "#<WIN32OLE::Record:#{@typename}>"
92
+ end
93
+
94
+ def method_missing(name, *args)
95
+ sname = name.to_s
96
+ case args.size
97
+ when 0 then @fields.fetch(sname)
98
+ when 1
99
+ key = sname.end_with?('=') ? sname[0..-2] : sname
100
+ @fields.fetch(key) # raises KeyError before writing, matching MRI
101
+ @fields[key] = args.first
102
+ else
103
+ super
104
+ end
105
+ end
106
+
107
+ def respond_to_missing?(name, include_private = false)
108
+ @fields.key?(name.to_s.sub(/=\z/, '')) || super
109
+ end
110
+
111
+ def ole_instance_variable_get(name)
112
+ unless name.is_a?(String) || name.is_a?(Symbol)
113
+ raise TypeError, 'wrong argument type (expected String or Symbol)'
114
+ end
115
+
116
+ @fields.fetch(name.to_s)
117
+ end
118
+
119
+ def ole_instance_variable_set(name, val)
120
+ unless name.is_a?(String) || name.is_a?(Symbol)
121
+ raise TypeError, 'wrong argument type (expected String or Symbol)'
122
+ end
123
+
124
+ key = name.to_s
125
+ @fields.fetch(key)
126
+ @fields[key] = val
127
+ end
128
+
129
+ VT_RECORD_BODY_SIZE = W::PTR_SIZE * 2 # BRECORD: { PVOID pvRecord; IRecordInfo *pRecInfo; }
130
+
131
+ def to_variant_bytes
132
+ size_out = ("\x00" * 4).b
133
+ hr = self.class.get_size_fn(@pri).call(@pri, size_out)
134
+ raise WIN32OLE::RuntimeError, "failed to get size for allocation of VT_RECORD object: #{W.hr_hex(hr)}" if W.failed?(hr)
135
+
136
+ # Port of ole_rec2variant (win32ole_record.c:101-103): free the
137
+ # previously-allocated buffer (if any) before replacing it, so a
138
+ # Record marshaled more than once doesn't leak one native buffer per
139
+ # call. The freed/replaced pointer lives in @finalizer_state, the same
140
+ # mutable Hash the GC finalizer (install_finalizer/self.finalizer)
141
+ # reads from -- so the finalizer always frees whatever buffer is
142
+ # current, without ever being re-registered.
143
+ Fiddle.free(@finalizer_state[:buffer_ptr]) if @finalizer_state[:buffer_ptr]
144
+
145
+ size = size_out.unpack1('L')
146
+ buffer_ptr = Fiddle::Pointer.malloc(size)
147
+ @finalizer_state[:buffer_ptr] = buffer_ptr
148
+ hr = self.class.record_init_fn(@pri).call(@pri, buffer_ptr.to_i)
149
+ raise WIN32OLE::RuntimeError, "failed to initialize VT_RECORD object: #{W.hr_hex(hr)}" if W.failed?(hr)
150
+
151
+ @fields.each do |name, val|
152
+ next if val.nil?
153
+
154
+ # Port of hash2olerec (win32ole_record.c:73-81): free the temporary
155
+ # VARIANT's own BSTR (there VariantClear, here to_free) right
156
+ # after PutField copies it, not batched at the end.
157
+ to_free = []
158
+ var_bytes = WIN32OLE.ruby_value_to_variant_bytes(val, to_free)
159
+ hr = self.class.put_field_fn(@pri).call(
160
+ @pri, W::DISPATCH_PROPERTYPUT, buffer_ptr.to_i, W.wstr(name), W.native_pointer_for(var_bytes)
161
+ )
162
+ to_free.each(&:call)
163
+ raise WIN32OLE::RuntimeError, "failed to putfield of `#{name}': #{W.hr_hex(hr)}" if W.failed?(hr)
164
+ end
165
+
166
+ body = [buffer_ptr.to_i, @pri].pack("#{W::PACK_PTR}2")
167
+ W.pack_variant(W::VT_RECORD, body)
168
+ end
169
+
170
+ def self.from_irecordinfo_and_buffer(pri, prec)
171
+ allocate.tap { |rec| rec.send(:set_record_info, pri, prec) }
172
+ end
173
+
174
+ private
175
+
176
+ def resolve_itypelib_ptr(oleobj)
177
+ case oleobj
178
+ when WIN32OLE::TypeLib
179
+ oleobj.instance_variable_get(:@ptr)
180
+ when WIN32OLE
181
+ oleobj.ole_typelib.instance_variable_get(:@ptr)
182
+ else
183
+ raise TypeError, "2nd argument should be WIN32OLE object or WIN32OLE::TypeLib object, got #{oleobj.class}"
184
+ end
185
+ end
186
+
187
+ # Port of recordinfo_from_itypelib (win32ole_record.c:29-59): linear-scan
188
+ # the typelib's own members for a name match (GetDocumentation), same
189
+ # already-open-typelib walk TypeLib#ole_types performs -- NOT a walk of
190
+ # the registry's TypeLib tree. Every non-matching
191
+ # ITypeInfo* GetTypeInfo AddRef'd along the way must be Released, or
192
+ # the scan leaks a reference per member it skips past.
193
+ def find_record_info(itypelib_ptr, typename)
194
+ count = TI.type_info_count_fn(itypelib_ptr).call(itypelib_ptr)
195
+ count.times do |i|
196
+ ti_out = ("\x00" * W::PTR_SIZE).b
197
+ next if W.failed?(TI.type_info_fn(itypelib_ptr).call(itypelib_ptr, i, ti_out))
198
+
199
+ itypeinfo_ptr = ti_out.unpack1(W::PACK_PTR)
200
+ name_out = ("\x00" * W::PTR_SIZE).b
201
+ TI.documentation_fn_for_typeinfo(itypeinfo_ptr).call(itypeinfo_ptr, -1, name_out, nil, nil, nil)
202
+ name_bstr = name_out.unpack1(W::PACK_PTR)
203
+ name = W.bstr_to_s(name_bstr)
204
+ W.sys_free_string.call(name_bstr) unless name_bstr.zero?
205
+
206
+ if name == typename
207
+ pri_out = ("\x00" * W::PTR_SIZE).b
208
+ hr = self.class.get_record_info_from_type_info_fn.call(itypeinfo_ptr, pri_out)
209
+ release_itypeinfo(itypeinfo_ptr)
210
+ return W.failed?(hr) ? nil : pri_out.unpack1(W::PACK_PTR)
211
+ end
212
+ release_itypeinfo(itypeinfo_ptr)
213
+ end
214
+ nil
215
+ end
216
+
217
+ def release_itypeinfo(itypeinfo_ptr)
218
+ W.vtable_function(itypeinfo_ptr, 2, [W::VOIDP], W::DWORD).call(itypeinfo_ptr)
219
+ end
220
+
221
+ # Shared by .new (prec = nil, every field starts nil) and
222
+ # .from_irecordinfo_and_buffer
223
+ # (prec = a real native buffer, fields read via GetFieldNoCopy). Ports
224
+ # olerecord_set_ivar (win32ole_record.c:122-169).
225
+ def set_record_info(pri, prec)
226
+ @pri = pri
227
+ # Mutable state the GC finalizer closure (below) shares a reference
228
+ # to -- #to_variant_bytes mutates buffer_ptr in place on
229
+ # every call. install_finalizer must run exactly once per instance
230
+ # (set_record_info itself only ever runs once, from .new or
231
+ # .from_irecordinfo_and_buffer): ObjectSpace.define_finalizer is
232
+ # additive, so re-registering here would run two finalizers at GC
233
+ # time and double-Release @pri.
234
+ @finalizer_state = { pri: pri, buffer_ptr: nil }
235
+ install_finalizer
236
+
237
+ name_out = ("\x00" * W::PTR_SIZE).b
238
+ if self.class.get_name_fn(pri).call(pri, name_out).zero?
239
+ bstr = name_out.unpack1(W::PACK_PTR)
240
+ @typename = W.bstr_to_s(bstr)
241
+ W.sys_free_string.call(bstr) unless bstr.zero?
242
+ end
243
+
244
+ count_out = ("\x00" * 4).b
245
+ hr = self.class.get_field_names_fn(pri).call(pri, count_out, nil)
246
+ count = count_out.unpack1('L')
247
+ @fields = {}
248
+ return if W.failed?(hr) || count.zero?
249
+
250
+ names_out = ("\x00" * (count * W::PTR_SIZE)).b
251
+ count_out = [count].pack('L')
252
+ self.class.get_field_names_fn(pri).call(pri, count_out, names_out)
253
+ bstrs = names_out.unpack(W::PACK_PTR * count)
254
+
255
+ bstrs.each do |bstr|
256
+ name = W.bstr_to_s(bstr)
257
+ val = nil
258
+ if prec
259
+ var_out = ("\x00" * W::VARIANT_SIZE).b
260
+ pdata_out = ("\x00" * W::PTR_SIZE).b
261
+ hr = self.class.get_field_no_copy_fn(pri).call(pri, prec, bstr, var_out, pdata_out)
262
+ val = WIN32OLE.variant_bytes_to_ruby_value(var_out) if hr.zero?
263
+ end
264
+ @fields[name] = val
265
+ W.sys_free_string.call(bstr) unless bstr.zero?
266
+ end
267
+ end
268
+
269
+ def install_finalizer
270
+ release_fn = W.vtable_function(@pri, 2, [W::VOIDP], W::DWORD)
271
+ ObjectSpace.define_finalizer(self, self.class.finalizer(@finalizer_state, release_fn))
272
+ end
273
+
274
+ # state is the same Hash to_variant_bytes mutates in place -- read
275
+ # state[:pri]/state[:buffer_ptr] fresh here (at GC time), never close
276
+ # over a value frozen when the proc was created, or a buffer allocated
277
+ # after this finalizer was installed would never be freed.
278
+ # The Release is OLE_FREE, deferred on a thread without OLE (as on
279
+ # JRuby; see WIN32OLE.finalizer); the buffer is freed right away.
280
+ def self.finalizer(state, release_fn)
281
+ proc do
282
+ pri = state[:pri]
283
+ W.release_on_ole_thread { release_fn.call(pri) } unless pri.nil? || pri.zero?
284
+ buffer_ptr = state[:buffer_ptr]
285
+ Fiddle.free(buffer_ptr) if buffer_ptr
286
+ end
287
+ end
288
+ end
289
+ end