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,384 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'win32ole/ffi/win32'
4
+ require 'win32ole/ffi/typeinfo'
5
+ require 'win32ole/ffi/type'
6
+
7
+ class WIN32OLE
8
+ class TypeLib
9
+ W = Win32
10
+ TI = TypeInfo
11
+ private_constant :W, :TI
12
+
13
+ LIBFLAG_FRESTRICTED = 0x1
14
+ LIBFLAG_FHIDDEN = 0x4
15
+
16
+ # Mirrors ext/win32ole/win32ole_typelib.c's foletypelib_initialize:
17
+ # +typelib+ is tried, in order, as a registered typelib name
18
+ # (major/minor ignored if this matches), then as a typelib GUID
19
+ # (major/minor pick a specific version, else the highest registered),
20
+ # then as a literal file path.
21
+ def self.new(*args)
22
+ unless (1..3).cover?(args.size)
23
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 1..3)"
24
+ end
25
+
26
+ typelib = check_string!(args[0])
27
+ file = file_from_name(typelib) || file_from_guid_and_version(typelib, args[1], args[2])
28
+ itypelib_ptr = load_typelib(file || typelib)
29
+ raise WIN32OLE::RuntimeError, "not found type library `#{typelib}`" if itypelib_ptr.nil?
30
+
31
+ allocate.tap { |tlib| tlib.send(:initialize, itypelib_ptr) }
32
+ end
33
+
34
+ def self.check_string!(val)
35
+ return val if val.is_a?(::String)
36
+ return val.to_str if val.respond_to?(:to_str)
37
+
38
+ raise TypeError, "no implicit conversion of #{val.class} into String"
39
+ end
40
+
41
+ def self.load_typelib(file)
42
+ out = ("\x00" * W::PTR_SIZE).b
43
+ hr = TI.load_type_lib_ex.call(W.wstr(file), TI::REGKIND_NONE, out)
44
+ return nil if W.failed?(hr)
45
+
46
+ out.unpack1(W::PACK_PTR)
47
+ end
48
+
49
+ # Mirrors oletypelib_search_registry2: +guid+ is a typelib GUID
50
+ # string (TypeLib\{guid} directly, not the CLSID\{clsid} root
51
+ # file_from_clsid uses). If major is given, only that exact version
52
+ # is accepted; otherwise the highest version with a registered name
53
+ # wins, same tie-break as file_from_name_under_guid.
54
+ def self.file_from_guid_and_version(guid, major, minor)
55
+ with_reg_subkey(TI::HKEY_CLASSES_ROOT, "TypeLib\\#{guid}") do |hguid|
56
+ version = version_string(major, minor)
57
+ if version
58
+ valid = with_reg_subkey(hguid, version) { |hversion| !reg_default_string_value(hversion).nil? }
59
+ version = nil unless valid
60
+ else
61
+ version = highest_named_version_under(hguid)
62
+ end
63
+ version && file_under_version_key(hguid, version)
64
+ end
65
+ end
66
+
67
+ def self.version_string(major, minor)
68
+ return nil if major.nil?
69
+
70
+ minor.nil? ? major.to_s : "#{major}.#{minor}"
71
+ end
72
+
73
+ def self.highest_named_version_under(hguid)
74
+ best_version = nil
75
+ best_fver = nil
76
+ i = 0
77
+ while (version = TI.reg_enum_key(hguid, i))
78
+ i += 1
79
+ fver = version.to_f
80
+ next if best_fver && fver <= best_fver
81
+
82
+ valid = with_reg_subkey(hguid, version) { |hversion| !reg_default_string_value(hversion).nil? }
83
+ if valid
84
+ best_fver = fver
85
+ best_version = version
86
+ end
87
+ end
88
+ best_version
89
+ end
90
+
91
+ def self.from_itypelib_ptr(itypelib_ptr)
92
+ allocate.tap { |tlib| tlib.send(:initialize, itypelib_ptr) }
93
+ end
94
+
95
+ def initialize(itypelib_ptr)
96
+ # As the C allocator does, so the finalizer's ole_initialized? check
97
+ # holds on the thread that made this object.
98
+ WIN32OLE.ole_initialize
99
+ @ptr = itypelib_ptr
100
+ # Install the finalizer before any call that could raise: the caller
101
+ # has already AddRef'd this pointer, so if GetLibAttr/read_documentation
102
+ # (or the vtable lookups they perform) raise, this Release must still
103
+ # happen -- otherwise the reference leaks permanently.
104
+ install_finalizer
105
+
106
+ lib_attr_out = ("\x00" * W::PTR_SIZE).b
107
+ hr = TI.lib_attr_fn(@ptr).call(@ptr, lib_attr_out)
108
+ if W.failed?(hr)
109
+ raise WIN32OLE::QueryInterfaceError, W.query_interface_error_message('GetLibAttr', W.hr_hex(hr))
110
+ end
111
+ attr_ptr = lib_attr_out.unpack1(W::PACK_PTR)
112
+ p = Fiddle::Pointer.new(attr_ptr)
113
+ @guid_bytes = p[0, 16]
114
+ @lcid = p[16, 4].unpack1('L')
115
+ @major = p[16 + 4 + 4, 2].unpack1('S') # skip lcid(4) + syskind(4, enum-sized)
116
+ @minor = p[16 + 4 + 4 + 2, 2].unpack1('S')
117
+ @lib_flags = p[16 + 4 + 4 + 2 + 2, 2].unpack1('S')
118
+ TI.release_tlib_attr_fn(@ptr).call(@ptr, attr_ptr)
119
+
120
+ @name, @helpstring, @help_context, @helpfile = read_documentation(@ptr, -1)
121
+ end
122
+
123
+ def guid
124
+ d1, d2, d3 = @guid_bytes.unpack('LSS')
125
+ d4 = @guid_bytes[8, 8].unpack('C8')
126
+ format('{%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}', d1, d2, d3, *d4)
127
+ end
128
+
129
+ def name
130
+ @helpstring
131
+ end
132
+ alias to_s name
133
+
134
+ def version
135
+ "#{@major}.#{@minor}"
136
+ end
137
+
138
+ def major_version
139
+ @major
140
+ end
141
+
142
+ def minor_version
143
+ @minor
144
+ end
145
+
146
+ def visible?
147
+ !(@lib_flags.zero? || (@lib_flags & (LIBFLAG_FRESTRICTED | LIBFLAG_FHIDDEN)) != 0)
148
+ end
149
+
150
+ def library_name
151
+ @name
152
+ end
153
+
154
+ def path
155
+ reg_path_lookup(guid, version, @lcid)
156
+ end
157
+
158
+ def ole_types
159
+ count = TI.type_info_count_fn(@ptr).call(@ptr)
160
+ Array.new(count) do |i|
161
+ ti_out = ("\x00" * W::PTR_SIZE).b
162
+ hr = TI.type_info_fn(@ptr).call(@ptr, i, ti_out)
163
+ next nil if W.failed?(hr)
164
+
165
+ begin
166
+ WIN32OLE::Type.from_typeinfo_ptr(ti_out.unpack1(W::PACK_PTR))
167
+ rescue WIN32OLE::QueryInterfaceError
168
+ # Matches MRI (ext/win32ole/win32ole_typelib.c's ole_types_from_typelib),
169
+ # which skips entries it can't build a Type for rather than
170
+ # failing the whole array -- one unreadable type in a typelib
171
+ # shouldn't break every other type (and, transitively, every
172
+ # object's #ole_methods) from that same library.
173
+ nil
174
+ end
175
+ end.compact
176
+ end
177
+
178
+ def inspect
179
+ "#<WIN32OLE::TypeLib:#{name}>"
180
+ end
181
+
182
+ alias ole_classes ole_types
183
+
184
+ # ext/win32ole/win32ole_typelib.c's foletypelib_s_typelibs: every
185
+ # HKCR\TypeLib\{guid}\{version} with a registered name whose file
186
+ # LoadTypeLibEx can open.
187
+ def self.typelibs
188
+ tlibs = []
189
+ with_reg_subkey(TI::HKEY_CLASSES_ROOT, 'TypeLib') do |htypelib|
190
+ each_reg_subkey_name(htypelib) do |guid|
191
+ with_reg_subkey(htypelib, guid) do |hguid|
192
+ each_reg_subkey_name(hguid) do |version|
193
+ next unless with_reg_subkey(hguid, version) { |hversion| reg_default_string_value(hversion) }
194
+
195
+ file = file_under_version_key(hguid, version)
196
+ ptr = file && load_typelib(file)
197
+ next unless ptr
198
+
199
+ begin
200
+ tlibs << from_itypelib_ptr(ptr)
201
+ rescue WIN32OLE::QueryInterfaceError
202
+ # unreadable TLIBATTR; its finalizer still releases ptr
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
208
+ tlibs
209
+ end
210
+
211
+ def self.each_reg_subkey_name(hkey)
212
+ i = 0
213
+ while (name = TI.reg_enum_key(hkey, i))
214
+ i += 1
215
+ yield name
216
+ end
217
+ end
218
+
219
+ # Mirrors ext/win32ole/win32ole_typelib.c's typelib_file: resolves a
220
+ # typelib name (e.g. "Microsoft Shell Controls And Automation") or a
221
+ # CLSID string (e.g. a COM class's own CLSID, whose InprocServer32 DLL
222
+ # commonly embeds the same typelib) to the file LoadTypeLibEx needs.
223
+ # Used by WIN32OLE::Type.new's name-based lookup. Returns nil if
224
+ # neither resolves -- the caller then tries the original string
225
+ # itself as a literal file path, same as the C fallback.
226
+ def self.file_for(typelib)
227
+ file_from_clsid(typelib) || file_from_name(typelib)
228
+ end
229
+
230
+ def self.file_from_clsid(clsid_str)
231
+ with_reg_subkey(TI::HKEY_CLASSES_ROOT, "CLSID\\#{clsid_str}\\InprocServer32") do |hkey|
232
+ reg_default_string_value(hkey)
233
+ end
234
+ end
235
+
236
+ def self.file_from_name(name)
237
+ with_reg_subkey(TI::HKEY_CLASSES_ROOT, 'TypeLib') do |htypelib|
238
+ result = nil
239
+ i = 0
240
+ while (guid = TI.reg_enum_key(htypelib, i))
241
+ i += 1
242
+ result = file_from_name_under_guid(htypelib, guid, name)
243
+ break if result
244
+ end
245
+ result
246
+ end
247
+ end
248
+
249
+ def self.file_from_name_under_guid(htypelib, guid, name)
250
+ with_reg_subkey(htypelib, guid) do |hclsid|
251
+ best_version = nil
252
+ best_fver = nil
253
+ i = 0
254
+ while (version = TI.reg_enum_key(hclsid, i))
255
+ i += 1
256
+ fver = version.to_f
257
+ next if best_fver && fver <= best_fver
258
+
259
+ matched = with_reg_subkey(hclsid, version) { |hversion| reg_default_string_value(hversion) == name }
260
+ if matched
261
+ best_fver = fver
262
+ best_version = version
263
+ end
264
+ end
265
+
266
+ best_version && file_under_version_key(hclsid, best_version)
267
+ end
268
+ end
269
+
270
+ def self.file_under_version_key(hclsid, version)
271
+ with_reg_subkey(hclsid, version) do |hversion|
272
+ result = nil
273
+ i = 0
274
+ while (lang = TI.reg_enum_key(hversion, i))
275
+ i += 1
276
+ result = with_reg_subkey(hversion, lang) { |hlang| reg_typelib_file_path(hlang) }
277
+ break if result
278
+ end
279
+ result
280
+ end
281
+ end
282
+
283
+ # Under a TypeLib\{guid}\{version}\{lcid} key, the actual file path is
284
+ # the default value of whichever of these architecture subkeys exists.
285
+ def self.reg_typelib_file_path(hlang)
286
+ %w[win64 win32 win16].each do |arch|
287
+ path = with_reg_subkey(hlang, arch) { |hkey| reg_default_string_value(hkey) }
288
+ return path if path
289
+ end
290
+ nil
291
+ end
292
+
293
+ def self.reg_default_string_value(hkey)
294
+ empty_name = W.wstr('')
295
+ type_out = ("\x00" * 4).b
296
+ size_out = [520].pack('L')
297
+ data_out = ("\x00" * 520).b
298
+ err = TI.reg_query_value_ex.call(hkey, empty_name, nil, type_out, data_out, size_out)
299
+ type = type_out.unpack1('L')
300
+ return nil unless err.zero? && (type == TI::REG_SZ || type == TI::REG_EXPAND_SZ)
301
+
302
+ value = data_out[0, size_out.unpack1('L')].force_encoding('UTF-16LE').encode('UTF-8').delete("\x00")
303
+ type == TI::REG_EXPAND_SZ ? W.expand_environment_strings(value) : value
304
+ end
305
+
306
+ def self.with_reg_subkey(hkey, name)
307
+ out = ("\x00" * W::PTR_SIZE).b
308
+ err = TI.reg_open_key_ex.call(hkey, W.wstr(name), 0, TI::KEY_READ, out)
309
+ return nil unless err.zero?
310
+
311
+ sub = out.unpack1(W::PACK_PTR)
312
+ begin
313
+ yield sub
314
+ ensure
315
+ TI.reg_close_key.call(sub)
316
+ end
317
+ end
318
+
319
+ private
320
+
321
+ def read_documentation(itypelib_ptr, index)
322
+ name_out = ("\x00" * W::PTR_SIZE).b
323
+ docstring_out = ("\x00" * W::PTR_SIZE).b
324
+ helpcontext_out = ("\x00" * 4).b
325
+ helpfile_out = ("\x00" * W::PTR_SIZE).b
326
+ TI.documentation_fn_for_typelib(itypelib_ptr).call(
327
+ itypelib_ptr, index, name_out, docstring_out, helpcontext_out, helpfile_out
328
+ )
329
+ name_bstr = name_out.unpack1(W::PACK_PTR)
330
+ docstring_bstr = docstring_out.unpack1(W::PACK_PTR)
331
+ helpfile_bstr = helpfile_out.unpack1(W::PACK_PTR)
332
+ name = W.bstr_to_s(name_bstr)
333
+ helpstring = W.bstr_to_s(docstring_bstr) || '' # C's WC2VSTR(NULL) is '', not nil
334
+ helpfile = W.bstr_to_s(helpfile_bstr) || ''
335
+ [name_bstr, docstring_bstr, helpfile_bstr].each { |b| W.sys_free_string.call(b) unless b.zero? }
336
+ [name, helpstring, helpcontext_out.unpack1('L'), helpfile]
337
+ end
338
+
339
+ def reg_path_lookup(guid_str, version_str, lcid)
340
+ # HKEY_CLASSES_ROOT is itself the merged Classes root, so the subkey
341
+ # path must NOT be prefixed with "SOFTWARE\Classes\" (that prefix is
342
+ # only needed when opening under HKEY_LOCAL_MACHINE/HKEY_CURRENT_USER
343
+ # directly, as MRI's own clsid_from_remote does for the DCOM path).
344
+ %w[win64 win32 win16].each do |arch_key|
345
+ path = reg_path_lookup_one(guid_str, version_str, lcid, arch_key)
346
+ return path if path
347
+ end
348
+ nil
349
+ end
350
+
351
+ def reg_path_lookup_one(guid_str, version_str, lcid, arch_key)
352
+ key_path = W.wstr("TypeLib\\#{guid_str}\\#{version_str}\\#{lcid}\\#{arch_key}")
353
+ hkey_out = ("\x00" * W::PTR_SIZE).b
354
+ err = TI.reg_open_key_ex.call(TI::HKEY_CLASSES_ROOT, key_path, 0, TI::KEY_READ, hkey_out)
355
+ return nil unless err.zero?
356
+
357
+ hkey = hkey_out.unpack1(W::PACK_PTR)
358
+ begin
359
+ empty_name = W.wstr('')
360
+ type_out = ("\x00" * 4).b
361
+ size_out = [520].pack('L') # 260 WCHARs, generous for a MAX_PATH-style value
362
+ data_out = ("\x00" * 520).b
363
+ err = TI.reg_query_value_ex.call(hkey, empty_name, nil, type_out, data_out, size_out)
364
+ return nil unless err.zero? && type_out.unpack1('L') == TI::REG_SZ
365
+
366
+ data_out[0, size_out.unpack1('L')].force_encoding('UTF-16LE').encode('UTF-8').delete("\x00")
367
+ ensure
368
+ TI.reg_close_key.call(hkey)
369
+ end
370
+ end
371
+
372
+ def install_finalizer
373
+ ptr = @ptr
374
+ release_fn = W.vtable_function(ptr, 2, [W::VOIDP], W::DWORD)
375
+ ObjectSpace.define_finalizer(self, self.class.finalizer(ptr, release_fn))
376
+ end
377
+
378
+ # OLE_FREE, deferred on a thread without OLE (as on JRuby); see
379
+ # WIN32OLE.finalizer.
380
+ def self.finalizer(ptr, release_fn)
381
+ proc { W.release_on_ole_thread { release_fn.call(ptr) } unless ptr.zero? }
382
+ end
383
+ end
384
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'win32ole/ffi/win32'
4
+ require 'win32ole/ffi/typeinfo'
5
+
6
+ class WIN32OLE
7
+ class Variable
8
+ W = Win32
9
+ TI = TypeInfo
10
+ private_constant :W, :TI
11
+
12
+ VARFLAG_FRESTRICTED = 0x80
13
+ VARFLAG_FHIDDEN = 0x40
14
+ VARFLAG_FNONBROWSABLE = 0x400
15
+ VAR_CONST = 2
16
+
17
+ # ext/win32ole/win32ole_variable.c undefines the allocator, so a
18
+ # Variable can't be constructed directly -- only via Type#variables.
19
+ def self.new(*)
20
+ raise TypeError, "allocator undefined for #{self}"
21
+ end
22
+
23
+ def self.from_typeinfo_ptr(itypeinfo_ptr, index)
24
+ allocate.tap { |var| var.send(:initialize, itypeinfo_ptr, index) }
25
+ end
26
+
27
+ def initialize(itypeinfo_ptr, index)
28
+ vardesc_out = ("\x00" * W::PTR_SIZE).b
29
+ hr = TI.var_desc_fn(itypeinfo_ptr).call(itypeinfo_ptr, index, vardesc_out)
30
+ if W.failed?(hr)
31
+ raise WIN32OLE::QueryInterfaceError, W.query_interface_error_message('GetVarDesc', W.hr_hex(hr))
32
+ end
33
+ vardesc_ptr = vardesc_out.unpack1(W::PACK_PTR)
34
+ vardesc = TI::VARDESC.new(vardesc_ptr)
35
+
36
+ @memid = vardesc.memid
37
+ @type_detail = TI.typedesc_details(itypeinfo_ptr, vardesc_ptr + TI.offsetof(TI::VARDESC, 'tdesc_union_ptr'))
38
+ @var_flags = vardesc.wVarFlags
39
+ @varkind = vardesc.varkind
40
+ @value = read_const_value(vardesc, vardesc_ptr) if @varkind == VAR_CONST
41
+ @name = read_name(itypeinfo_ptr, @memid)
42
+
43
+ TI.release_var_desc_fn(itypeinfo_ptr).call(itypeinfo_ptr, vardesc_ptr)
44
+ end
45
+
46
+ def name
47
+ @name
48
+ end
49
+ alias to_s name
50
+
51
+ def ole_type
52
+ @type_detail.last
53
+ end
54
+
55
+ def ole_type_detail
56
+ @type_detail.dup
57
+ end
58
+
59
+ def value
60
+ @value
61
+ end
62
+
63
+ def visible?
64
+ (@var_flags & (VARFLAG_FRESTRICTED | VARFLAG_FHIDDEN | VARFLAG_FNONBROWSABLE)) == 0
65
+ end
66
+
67
+ def variable_kind
68
+ TI::VARKIND_NAMES.fetch(@varkind, 'UNKNOWN')
69
+ end
70
+
71
+ def varkind
72
+ @varkind
73
+ end
74
+
75
+ def inspect
76
+ "#<WIN32OLE::Variable:#{name}=#{value.inspect}>"
77
+ end
78
+
79
+ private
80
+
81
+ def read_const_value(vardesc, vardesc_ptr)
82
+ # VARDESC.union_oInst_or_lpvarValue holds a VARIANT* when varkind == VAR_CONST.
83
+ variant_ptr = vardesc.union_oInst_or_lpvarValue
84
+ return nil if variant_ptr.to_i.zero?
85
+
86
+ variant_bytes = Fiddle::Pointer.new(variant_ptr)[0, W::VARIANT_SIZE]
87
+ vt, payload = W.unpack_variant(variant_bytes)
88
+ type = W.variant_ruby_type(vt)
89
+ case type
90
+ when :i4 then W.unpack_i4(payload)
91
+ when :i8 then W.unpack_i8(payload)
92
+ when :r8 then W.unpack_r8(payload)
93
+ when :bool then W.unpack_bool(payload)
94
+ when :bstr then W.bstr_to_s(W.unpack_pointer(payload))
95
+ else nil
96
+ end
97
+ end
98
+
99
+ def read_name(itypeinfo_ptr, memid)
100
+ names_out = ("\x00" * W::PTR_SIZE).b
101
+ count_out = ("\x00" * 4).b
102
+ get_names_fn(itypeinfo_ptr).call(itypeinfo_ptr, memid, names_out, 1, count_out)
103
+ bstr = names_out.unpack1(W::PACK_PTR)
104
+ name = W.bstr_to_s(bstr)
105
+ W.sys_free_string.call(bstr) unless bstr.zero?
106
+ name
107
+ end
108
+
109
+ def get_names_fn(itypeinfo_ptr)
110
+ @@get_names_fns ||= {}
111
+ @@get_names_fns[W.vtable_address(itypeinfo_ptr)] ||= W.vtable_function(
112
+ itypeinfo_ptr, TI::ITYPEINFO_VTBL[:GetNames],
113
+ [W::VOIDP, W::LONG, W::VOIDP, W::DWORD, W::VOIDP], W::LONG
114
+ )
115
+ end
116
+ end
117
+ end