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,351 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fiddle'
4
+ require 'win32ole/ffi/win32'
5
+ require 'win32ole/ffi/array'
6
+
7
+ class WIN32OLE
8
+ module VariantType
9
+ VT_EMPTY = 0
10
+ VT_NULL = 1
11
+ VT_I2 = 2
12
+ VT_I4 = 3
13
+ VT_R4 = 4
14
+ VT_R8 = 5
15
+ VT_CY = 6
16
+ VT_DATE = 7
17
+ VT_BSTR = 8
18
+ VT_DISPATCH = 9
19
+ VT_ERROR = 10
20
+ VT_BOOL = 11
21
+ VT_VARIANT = 12
22
+ VT_UNKNOWN = 13
23
+ VT_DECIMAL = 14
24
+ VT_I1 = 16
25
+ VT_UI1 = 17
26
+ VT_UI2 = 18
27
+ VT_UI4 = 19
28
+ VT_I8 = 20
29
+ VT_UI8 = 21
30
+ VT_INT = 22
31
+ VT_UINT = 23
32
+ VT_VOID = 24
33
+ VT_HRESULT = 25
34
+ VT_PTR = 26
35
+ VT_SAFEARRAY = 27
36
+ VT_CARRAY = 28
37
+ VT_USERDEFINED = 29
38
+ VT_LPSTR = 30
39
+ VT_LPWSTR = 31
40
+ VT_RECORD = 36
41
+ VT_INT_PTR = 37
42
+ VT_UINT_PTR = 38
43
+ VT_FILETIME = 64
44
+ VT_BLOB = 65
45
+ VT_STREAM = 66
46
+ VT_STORAGE = 67
47
+ VT_STREAMED_OBJECT = 68
48
+ VT_STORED_OBJECT = 69
49
+ VT_BLOB_OBJECT = 70
50
+ VT_CF = 71
51
+ VT_CLSID = 72
52
+ VT_VERSIONED_STREAM = 73
53
+ VT_BSTR_BLOB = 0xFFF
54
+ VT_VECTOR = 0x1000
55
+ VT_ARRAY = 0x2000
56
+ VT_BYREF = 0x4000
57
+ VT_RESERVED = 0x8000
58
+ VT_ILLEGAL = 0xFFFF
59
+ VT_ILLEGALMASKED = 0xFFF
60
+ VT_TYPEMASK = 0xFFF
61
+ end
62
+
63
+ VARIANT = VariantType
64
+
65
+ class Variant
66
+ W = Win32
67
+ SA = SafeArray
68
+ VT = VariantType
69
+ private_constant :W, :SA, :VT
70
+
71
+ def initialize(val, vartype = nil, _reserved = nil)
72
+ if vartype.nil?
73
+ to_free = []
74
+ @var = own(WIN32OLE.ruby_value_to_variant_bytes(val, to_free), to_free)
75
+ track_realvar(@var)
76
+ return
77
+ end
78
+
79
+ raise ArgumentError, 'WIN32OLE::Variant does not support VT_RECORD; use WIN32OLE::Record instead' if (vartype & VT::VT_TYPEMASK) == VT::VT_RECORD
80
+
81
+ # ole_val_ary2variant_ary: a plain VT_ARRAY is an array of VARIANTs.
82
+ vartype |= VT::VT_VARIANT if (vartype & ~VT::VT_BYREF) == VT::VT_ARRAY
83
+ base_vt = vartype & VT::VT_TYPEMASK
84
+ byref = (vartype & VT::VT_BYREF) != 0
85
+
86
+ realvar_bytes =
87
+ if (vartype & VT::VT_ARRAY) != 0
88
+ # nil: the type with no SAFEARRAY behind it, as C leaves it
89
+ psa = val.nil? ? 0 : with_temporaries { |to_free| SA.ruby_array_to_safearray(val, base_vt, to_free).to_i }
90
+ W.pack_variant(vartype & ~VT::VT_BYREF, W.pack_pointer(psa))
91
+ else
92
+ scalar_realvar(val, vartype)
93
+ end
94
+
95
+ @realvar = byref ? persist_realvar(realvar_bytes) : realvar_bytes
96
+ @var = byref ? W.pack_byref(vartype & ~VT::VT_BYREF, @realvar) : @realvar
97
+ byref ? commit_realvar(@realvar) : track_realvar(@realvar)
98
+ end
99
+
100
+ def value
101
+ vt, = W.unpack_variant(@var)
102
+ base_vt = vt & VT::VT_TYPEMASK
103
+ if (vt & VT::VT_ARRAY) != 0 && base_vt == VT::VT_UI1
104
+ psa = current_array_ptr
105
+ return SA.ui1_safearray_to_bytes(psa) if !psa.zero? && SA.safe_array_get_dim.call(psa) == 1
106
+ end
107
+
108
+ # The Variant keeps owning what @var holds (folevariant_value copies).
109
+ WIN32OLE.variant_bytes_to_ruby_value(@var, owned: false)
110
+ end
111
+
112
+ def value=(val)
113
+ vt, = W.unpack_variant(@var)
114
+ base_vt = vt & VT::VT_TYPEMASK
115
+ byref = (vt & VT::VT_BYREF) != 0
116
+ realvar_bytes =
117
+ if (vt & VT::VT_ARRAY) != 0
118
+ unless val.is_a?(::String) && base_vt == VT::VT_UI1
119
+ raise WIN32OLE::RuntimeError, 'array value can only be replaced with a String for a VT_UI1|VT_ARRAY Variant'
120
+ end
121
+
122
+ psa = SA.ruby_array_to_safearray(val, base_vt)
123
+ W.pack_variant(vt & ~VT::VT_BYREF, W.pack_pointer(psa.to_i))
124
+ else
125
+ scalar_realvar(val, vt)
126
+ end
127
+ @realvar = byref ? persist_realvar(realvar_bytes) : realvar_bytes
128
+ @var = byref ? W.pack_byref(vt & ~VT::VT_BYREF, @realvar) : @realvar
129
+ byref ? commit_realvar(@realvar) : track_realvar(@realvar)
130
+ end
131
+
132
+ def vartype
133
+ vt, = W.unpack_variant(@var)
134
+ vt
135
+ end
136
+
137
+ def self.array(dims, vt)
138
+ raise TypeError, "wrong argument type #{dims.class} (expected Array)" unless dims.is_a?(::Array)
139
+
140
+ bounds = dims.flat_map { |n| [n, 0] }.pack('L2' * dims.size)
141
+ psa = SA.safe_array_create.call(vt & VT::VT_TYPEMASK, dims.size, bounds)
142
+ raise ::RuntimeError, 'memory allocation error' if psa.nil? || psa.to_i.zero?
143
+
144
+ allocate.tap { |v| v.send(:set_array_var, psa, vt) }
145
+ end
146
+
147
+ def [](*indices)
148
+ base_vt, psa = array_state
149
+ dim = SA.safe_array_get_dim.call(psa)
150
+ raise ArgumentError, 'unmatch number of indices' unless indices.size == dim
151
+
152
+ hr = SA.safe_array_lock.call(psa)
153
+ raise WIN32OLE::RuntimeError, "failed to SafeArrayLock: #{W.hr_hex(hr)}" if W.failed?(hr)
154
+
155
+ begin
156
+ index_buf = indices.pack('l' * indices.size)
157
+ elem_ptr_out = ("\x00" * W::PTR_SIZE).b
158
+ hr = SA.safe_array_ptr_of_index.call(psa, index_buf, elem_ptr_out)
159
+ raise WIN32OLE::RuntimeError, "failed to SafeArrayPtrOfIndex: #{W.hr_hex(hr)}" if W.failed?(hr)
160
+
161
+ SA.read_element(elem_ptr_out.unpack1(W::PACK_PTR), base_vt)
162
+ ensure
163
+ SA.safe_array_unlock.call(psa)
164
+ end
165
+ end
166
+
167
+ def []=(*args)
168
+ val = args.pop
169
+ indices = args
170
+ base_vt, psa = array_state
171
+ dim = SA.safe_array_get_dim.call(psa)
172
+ raise ArgumentError, 'unmatch number of indices' unless indices.size == dim
173
+
174
+ hr = SA.safe_array_lock.call(psa)
175
+ raise WIN32OLE::RuntimeError, "failed to SafeArrayLock: #{W.hr_hex(hr)}" if W.failed?(hr)
176
+
177
+ begin
178
+ with_temporaries { |to_free| SA.write_element(psa, indices.pack('l' * indices.size), base_vt, val, to_free) }
179
+ val
180
+ ensure
181
+ SA.safe_array_unlock.call(psa)
182
+ end
183
+ end
184
+
185
+ private
186
+
187
+ def current_array_ptr
188
+ vt, payload = W.unpack_variant(@var)
189
+ addr = W.unpack_pointer(payload)
190
+ return addr if (vt & VT::VT_BYREF).zero?
191
+
192
+ Fiddle::Pointer.new(addr)[0, W::PTR_SIZE].unpack1(W::PACK_PTR)
193
+ end
194
+
195
+ # ext/win32ole/win32ole_variant.c's ole_val2olevariantdata, non-array
196
+ # branch: nil is the zero of the requested type; VT_ERROR takes the
197
+ # SCODE as is; anything else is converted to its natural VARIANT (I8/UI8
198
+ # packed directly, as ole_val2variant_ex does) and, when that type
199
+ # differs, coerced with VariantChangeTypeEx. VT_VARIANT keeps the
200
+ # natural VARIANT.
201
+ def scalar_realvar(val, vt)
202
+ base_vt = vt & ~VT::VT_BYREF
203
+ return error_realvar(val) if base_vt == VT::VT_ERROR
204
+ if val.nil?
205
+ return W.pack_variant(VT::VT_EMPTY, W.pack_empty) if vt == (VT::VT_VARIANT | VT::VT_BYREF)
206
+
207
+ return W.pack_variant(base_vt, W.pack_empty)
208
+ end
209
+
210
+ to_free = []
211
+ natural =
212
+ case base_vt
213
+ when VT::VT_I8 then W.pack_variant(base_vt, W.pack_i8(val))
214
+ when VT::VT_UI8 then W.pack_variant(base_vt, W.pack_ui8(val))
215
+ else WIN32OLE.ruby_value_to_variant_bytes(val, to_free)
216
+ end
217
+ natural_vt, = W.unpack_variant(natural)
218
+ return own(natural, to_free) if base_vt == VT::VT_VARIANT || natural_vt == base_vt
219
+
220
+ # VariantChangeTypeEx makes a VARIANT of its own.
221
+ with_temporaries(to_free) { W.change_variant_type(natural, base_vt) }
222
+ end
223
+
224
+ # Runs the block with a list for the temporary allocations of the
225
+ # VARIANTs it builds, and frees them once it's done.
226
+ def with_temporaries(to_free = [])
227
+ yield to_free
228
+ ensure
229
+ to_free.each(&:call)
230
+ end
231
+
232
+ # +var+ as a VARIANT owning what it holds: a VariantCopy, when the
233
+ # temporaries in +to_free+ (freed here) own it.
234
+ def own(var, to_free)
235
+ return var if to_free.empty?
236
+
237
+ with_temporaries(to_free) { W.variant_copy(var) }
238
+ end
239
+
240
+ # ole_val2variant_err: an Integer SCODE (or nil for 0), possibly held
241
+ # in another Variant.
242
+ def error_realvar(val)
243
+ scode = val.is_a?(Variant) ? val.value : val
244
+ unless scode.nil? || scode.is_a?(::Integer)
245
+ raise WIN32OLE::RuntimeError, "failed to convert VT_ERROR VARIANT:`#{scode.inspect}'"
246
+ end
247
+
248
+ W.pack_variant(VT::VT_ERROR, W.pack_error(scode || 0))
249
+ end
250
+
251
+ def set_array_var(psa, vt)
252
+ vt |= VT::VT_ARRAY
253
+ realvar_bytes = W.pack_variant(vt & ~VT::VT_BYREF, W.pack_pointer(psa.to_i))
254
+ byref = (vt & VT::VT_BYREF) != 0
255
+ @realvar = byref ? persist_realvar(realvar_bytes) : realvar_bytes
256
+ @var = byref ? W.pack_byref(vt & ~VT::VT_BYREF, @realvar) : @realvar
257
+ byref ? commit_realvar(@realvar) : track_realvar(@realvar)
258
+ end
259
+
260
+ # Allocates a persistent buffer for bytes and returns it -- does NOT
261
+ # touch @realvar_finalizer_state yet. Call commit_realvar with the
262
+ # result once @var/@realvar have been rebuilt to reference it, so the
263
+ # old buffer (if any) is only freed after nothing references it
264
+ # anymore -- freeing it first (the naive order) leaves a window where
265
+ # @var's own embedded bytes point at already-freed memory, and if
266
+ # allocation itself raises, freeing the old buffer up front would
267
+ # leave the finalizer holding an already-freed pointer (double-free
268
+ # at GC time).
269
+ def persist_realvar(bytes)
270
+ realvar_state
271
+ W.persistent_pointer_for(bytes)
272
+ end
273
+
274
+ def realvar_state
275
+ @realvar_finalizer_state ||= {}.tap do |state|
276
+ ObjectSpace.define_finalizer(self, self.class.realvar_finalizer(state))
277
+ end
278
+ end
279
+
280
+ # The non-VT_BYREF counterpart of commit_realvar: +bytes+ is now the
281
+ # realvar, and what the one before held is cleared.
282
+ def track_realvar(bytes)
283
+ old = realvar_state[:bytes]
284
+ realvar_state[:bytes] = bytes
285
+ self.class.clear_realvar(old) if old
286
+ end
287
+
288
+ # Stores ptr as the current realvar buffer and frees whatever was
289
+ # there before -- call this only after @var/@realvar already
290
+ # reference ptr, not before.
291
+ def commit_realvar(ptr)
292
+ old_ptr = @realvar_finalizer_state[:ptr]
293
+ @realvar_finalizer_state[:ptr] = ptr
294
+ return unless old_ptr
295
+
296
+ self.class.clear_realvar(old_ptr)
297
+ Fiddle.free(old_ptr)
298
+ end
299
+
300
+ # olevariant_free: VariantClear what the realvar holds now (for
301
+ # VT_BYREF, whatever the buffer holds -- a callee may have replaced it),
302
+ # then free the buffer. Runs at most once.
303
+ # Not deferred like the Release finalizers: as C's dfree does, this
304
+ # calls VariantClear on whatever thread GC picks (on JRuby, one without
305
+ # OLE).
306
+ def self.realvar_finalizer(state)
307
+ proc do
308
+ if (ptr = state.delete(:ptr))
309
+ clear_realvar(ptr)
310
+ Fiddle.free(ptr)
311
+ end
312
+ bytes = state.delete(:bytes)
313
+ clear_realvar(bytes) if bytes
314
+ end
315
+ end
316
+
317
+ # VariantClear, skipped for the types that own nothing.
318
+ def self.clear_realvar(var)
319
+ bytes = var.is_a?(::String) ? var : Fiddle::Pointer.new(var.to_i)[0, W::VARIANT_SIZE]
320
+ vt, = W.unpack_variant(bytes)
321
+ owns = (vt & VT::VT_ARRAY) != 0 ||
322
+ [VT::VT_BSTR, VT::VT_DISPATCH, VT::VT_UNKNOWN, VT::VT_RECORD].include?(vt & VT::VT_TYPEMASK)
323
+ W.variant_clear(var) if owns
324
+ end
325
+
326
+ def array_state
327
+ vt, payload = W.unpack_variant(@var)
328
+ addr = W.unpack_pointer(payload)
329
+ addr = Fiddle::Pointer.new(addr)[0, W::PTR_SIZE].unpack1(W::PACK_PTR) if (vt & VT::VT_BYREF) != 0
330
+ [vt & VT::VT_TYPEMASK, addr]
331
+ end
332
+
333
+ DISP_E_PARAMNOTFOUND = -2147352572 # 0x80020004
334
+
335
+ # Empty/Null/Nothing/NoParam are lazy, not eager: NoParam's construction
336
+ # calls VariantChangeTypeEx (a real Fiddle.dlopen('oleaut32') on first
337
+ # use), and every other native call in this codebase is lazy/memoized so
338
+ # that simply requiring the gem is harmless on any platform. const_missing
339
+ # preserves the exact same WIN32OLE::Variant::Empty-style constant-access
340
+ # API MRI provides, while deferring construction to first actual access.
341
+ def self.const_missing(name)
342
+ case name
343
+ when :Empty then const_set(:Empty, new(nil, VariantType::VT_EMPTY))
344
+ when :Null then const_set(:Null, new(nil, VariantType::VT_NULL))
345
+ when :Nothing then const_set(:Nothing, new(nil, VariantType::VT_DISPATCH))
346
+ when :NoParam then const_set(:NoParam, new(DISP_E_PARAMNOTFOUND, VariantType::VT_ERROR))
347
+ else super
348
+ end
349
+ end
350
+ end
351
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Win32OLEFFI
4
+ VERSION = "0.1.0"
5
+ end