win32ole-ffi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/BSDL +22 -0
- data/CHANGELOG.md +10 -0
- data/COPYING +56 -0
- data/README.md +49 -0
- data/lib/win32ole/ffi/array.rb +312 -0
- data/lib/win32ole/ffi/dispatch.rb +125 -0
- data/lib/win32ole/ffi/event.rb +800 -0
- data/lib/win32ole/ffi/fiddle_compat.rb +49 -0
- data/lib/win32ole/ffi/method.rb +237 -0
- data/lib/win32ole/ffi/param.rb +111 -0
- data/lib/win32ole/ffi/record.rb +289 -0
- data/lib/win32ole/ffi/type.rb +337 -0
- data/lib/win32ole/ffi/typeinfo.rb +488 -0
- data/lib/win32ole/ffi/typelib.rb +384 -0
- data/lib/win32ole/ffi/variable.rb +117 -0
- data/lib/win32ole/ffi/variant.rb +351 -0
- data/lib/win32ole/ffi/version.rb +5 -0
- data/lib/win32ole/ffi/win32.rb +907 -0
- data/lib/win32ole/ffi/win32ole.rb +827 -0
- data/lib/win32ole/ffi.rb +48 -0
- data/sig/win32ole/ffi.rbs +308 -0
- metadata +84 -0
|
@@ -0,0 +1,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
|