wrapture 0.2.2 → 0.5.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.
@@ -1,74 +1,385 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
1
3
  # frozen_string_literal: true
2
4
 
5
+ #--
6
+ # Copyright 2019-2020 Joel E. Anderson
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+ #++
20
+
3
21
  module Wrapture
4
- ##
5
22
  # A description of a function to be generated, including details about the
6
23
  # underlying implementation.
7
24
  class FunctionSpec
25
+ # Returns a copy of the return type specification +spec+.
26
+ def self.normalize_return_hash(spec)
27
+ if spec.nil?
28
+ { 'type' => 'void', 'includes' => [] }
29
+ else
30
+ normalized = Marshal.load(Marshal.dump(spec))
31
+ Comment.validate_doc(spec['doc']) if spec.key?('doc')
32
+ normalized['type'] ||= 'void'
33
+ normalized['includes'] = Wrapture.normalize_includes(spec['includes'])
34
+ normalized
35
+ end
36
+ end
37
+
38
+ # Normalizes a hash specification of a function. Normalization will check
39
+ # for things like invalid keys, duplicate entries in include lists, and will
40
+ # set missing keys to their default values (for example, an empty list if no
41
+ # includes are given).
8
42
  def self.normalize_spec_hash(spec)
9
- normalized_spec = spec.dup
43
+ Comment.validate_doc(spec['doc']) if spec.key?('doc')
44
+
45
+ normalized = spec.dup
46
+
47
+ normalized['version'] = Wrapture.spec_version(spec)
48
+ normalized['virtual'] = Wrapture.normalize_boolean(spec, 'virtual')
49
+ normalized['params'] = ParamSpec.normalize_param_list(spec['params'])
50
+ normalized['return'] = normalize_return_hash(spec['return'])
51
+
52
+ overload = Wrapture.normalize_boolean(normalized['return'], 'overloaded')
53
+ normalized['return']['overloaded'] = overload
54
+
55
+ normalized
56
+ end
57
+
58
+ # Creates a function spec based on the provided function spec.
59
+ #
60
+ # The hash must have the following keys:
61
+ # name:: the name of the function
62
+ # params:: a list of parameter specifications
63
+ # wrapped-function:: a hash describing the function to be wrapped
64
+ #
65
+ # Each parameter specification must have a 'name' key with the name of the
66
+ # parameter and a 'type' key with its type. The type key may be ommitted
67
+ # if the name of the parameter is '...' in which case the generated function
68
+ # will be made variadic. It may optionally have an 'includes' key with
69
+ # includes that are required (for example to support the type) and/or a
70
+ # 'doc' key with documentation of the parameter.
71
+ #
72
+ # Only one parameter named '...' is allowed in a specification. If more than
73
+ # one is provided, then only the first encountered will be used. This
74
+ # parameter should also be last - if it is not, it will be moved to the end
75
+ # of the parameter list during normalization.
76
+ #
77
+ # The wrapped-function must have a 'name' key with the name of the function,
78
+ # and a 'params' key with a list of parameters (each a hash with a 'name'
79
+ # and 'type' key). Optionally, it may also include an 'includes' key with a
80
+ # list of includes that are needed for this function to compile. The wrapped
81
+ # function may be left out entirely, but the function will not be definable
82
+ # if this is the case.
83
+ #
84
+ # The following keys are optional:
85
+ # doc:: a string containing the documentation for this function
86
+ # return:: a specification of the return value for this function
87
+ # static:: set to true if this is a static function
88
+ #
89
+ # The return specification may have either a 'type' key with the name of the
90
+ # type the function returns, and/or a 'doc' key with documentation on the
91
+ # return value itself. If neither of these is needed, then the return
92
+ # specification may simply be omitted.
93
+ #
94
+ # The 'type' key of the return spec may also be set to 'self-reference'
95
+ # which will have the function return a reference to the instance it was
96
+ # called on. Of course, this cannot be used from a function that is not a
97
+ # class method.
98
+ def initialize(spec, owner = Scope.new, constructor: false,
99
+ destructor: false)
100
+ @owner = owner
101
+ @spec = FunctionSpec.normalize_spec_hash(spec)
102
+ @wrapped = if @spec.key?('wrapped-function')
103
+ WrappedFunctionSpec.new(@spec['wrapped-function'])
104
+ end
105
+ @params = ParamSpec.new_list(@spec['params'])
106
+ @return_type = TypeSpec.new(@spec['return']['type'])
107
+ @constructor = constructor
108
+ @destructor = destructor
109
+ end
110
+
111
+ # A TypeSpec describing the return type of this function.
112
+ attr_reader :return_type
113
+
114
+ # True if the function is a constructor, false otherwise.
115
+ def constructor?
116
+ @constructor
117
+ end
118
+
119
+ # A list of includes needed for the declaration of the function.
120
+ def declaration_includes
121
+ includes = @spec['return']['includes'].dup
122
+ @params.each { |param| includes.concat(param.includes) }
123
+ includes.concat(@return_type.includes)
124
+ includes.uniq
125
+ end
126
+
127
+ # True if this function can be defined.
128
+ def definable?
129
+ definable_check
130
+ rescue UndefinableSpec
131
+ false
132
+ end
133
+
134
+ # A list of includes needed for the definition of the function.
135
+ def definition_includes
136
+ includes = @wrapped.includes
137
+ includes.concat(@spec['return']['includes'])
138
+ @params.each { |param| includes.concat(param.includes) }
139
+ includes.concat(@return_type.includes)
140
+ includes << 'stdarg.h' if variadic?
141
+ includes.uniq
142
+ end
143
+
144
+ # The name of the function.
145
+ def name
146
+ @spec['name']
147
+ end
148
+
149
+ # A string with the parameter list for this function.
150
+ def param_list
151
+ ParamSpec.signature(@params, self)
152
+ end
153
+
154
+ # The name of the function with the class name, if it exists.
155
+ def qualified_name
156
+ if @owner.is_a?(ClassSpec)
157
+ "#{@owner.name}::#{name}"
158
+ else
159
+ name
160
+ end
161
+ end
162
+
163
+ # Gives an expression for calling a given parameter within this function.
164
+ # Equivalent structs and pointers are resolved, as well as casts between
165
+ # types if they are known within the scope of this function.
166
+ def resolve_wrapped_param(param_spec)
167
+ used_param = @params.find { |p| p.name == param_spec['value'] }
168
+
169
+ if param_spec['value'] == EQUIVALENT_STRUCT_KEYWORD
170
+ @owner.this_struct
171
+ elsif param_spec['value'] == EQUIVALENT_POINTER_KEYWORD
172
+ @owner.this_struct_pointer
173
+ elsif param_spec['value'] == '...'
174
+ 'variadic_args'
175
+ elsif castable?(param_spec)
176
+ param_class = @owner.type(used_param.type)
177
+ param_class.cast(used_param.name,
178
+ param_spec['type'],
179
+ used_param.type)
180
+ else
181
+ param_spec['value']
182
+ end
183
+ end
184
+
185
+ # Calls return_expression on the return type of this function. +func_name+
186
+ # is passed to return_expression if provided.
187
+ def return_expression(func_name: name)
188
+ if @constructor || @destructor
189
+ signature(func_name: func_name)
190
+ else
191
+ resolved_return.return_expression(self, func_name: func_name)
192
+ end
193
+ end
10
194
 
11
- normalized_spec['params'] ||= []
12
- normalized_spec['wrapped-function']['params'] ||= []
13
- normalized_spec['wrapped-function']['includes'] ||= []
195
+ # The signature of the function. +func_name+ can be used to override the
196
+ # function name if needed, for example if a class name qualifier is needed.
197
+ def signature(func_name: name)
198
+ "#{func_name}( #{param_list} )"
199
+ end
200
+
201
+ # Calls the given block once for each line of the declaration of the
202
+ # function, including any documentation.
203
+ def declaration(&block)
204
+ doc.format_as_doxygen(max_line_length: 76) { |line| block.call(line) }
205
+
206
+ modifier_prefix = if @spec['static']
207
+ 'static '
208
+ elsif virtual?
209
+ 'virtual '
210
+ else
211
+ ''
212
+ end
213
+
214
+ block.call("#{modifier_prefix}#{return_expression};")
215
+ end
216
+
217
+ # Gives the definition of the function in a block, line by line.
218
+ def definition
219
+ definable_check
14
220
 
15
- if normalized_spec['return'].nil?
16
- normalized_spec['return'] = {}
17
- normalized_spec['return']['type'] = 'void'
221
+ yield "#{return_expression(func_name: qualified_name)} {"
222
+
223
+ locals { |declaration| yield " #{declaration}" }
224
+
225
+ yield " va_start( variadic_args, #{@params[-2].name} );" if variadic?
226
+
227
+ if @wrapped.error_check?
228
+ yield
229
+ yield " #{wrapped_call_expression};"
230
+ yield
231
+ @wrapped.error_check(return_val: return_variable) do |line|
232
+ yield " #{line}"
233
+ end
234
+ else
235
+ yield " #{wrapped_call_expression};"
18
236
  end
19
237
 
20
- normalized_spec['return']['includes'] ||= []
238
+ yield ' va_end( variadic_args );' if variadic?
239
+
240
+ if @return_type.self?
241
+ yield ' return *this;'
242
+ elsif @spec['return']['type'] != 'void' && !returns_call_directly?
243
+ yield ' return return_val;'
244
+ end
21
245
 
22
- normalized_spec
246
+ yield '}'
23
247
  end
24
248
 
25
- def self.param_list(spec)
26
- return 'void' if spec['params'].empty?
249
+ # A Comment holding the function documentation.
250
+ def doc
251
+ comment = String.new
252
+ comment << @spec['doc'] if @spec.key?('doc')
27
253
 
28
- params = []
254
+ @params
255
+ .reject { |param| param.doc.empty? }
256
+ .each { |param| comment << "\n\n" << param.doc.text }
29
257
 
30
- spec['params'].each do |param|
31
- params << ClassSpec.typed_variable(param['type'], param['name'])
258
+ if @spec['return'].key?('doc')
259
+ comment << "\n\n@return " << @spec['return']['doc']
32
260
  end
33
261
 
34
- params.join ', '
262
+ Comment.new(comment)
35
263
  end
36
264
 
37
- def initialize(spec, owner)
38
- @owner = owner
39
- @spec = FunctionSpec.normalize_spec_hash(spec)
265
+ # A resolved type, given a TypeSpec +type+. Resolved types will not have any
266
+ # keywords like +equivalent-struct+, which will be resolved to their
267
+ # effective type.
268
+ def resolve_type(type)
269
+ if type.equivalent_struct?
270
+ TypeSpec.new("struct #{@owner.struct_name}")
271
+ elsif type.equivalent_pointer?
272
+ TypeSpec.new("struct #{@owner.struct_name} *")
273
+ elsif type.self?
274
+ TypeSpec.new("#{@owner.name}&")
275
+ else
276
+ type
277
+ end
40
278
  end
41
279
 
42
- def declaration_includes
43
- @spec['return']['includes'].dup
280
+ # True if the function is variadic.
281
+ def variadic?
282
+ @params.last&.variadic?
44
283
  end
45
284
 
46
- def definition_includes
47
- includes = @spec['return']['includes'].dup
48
- includes.concat @spec['wrapped-function']['includes']
285
+ # True if the function is virtual.
286
+ def virtual?
287
+ @spec['virtual']
288
+ end
49
289
 
50
- includes.uniq
290
+ private
291
+
292
+ # True if the return value of the wrapped call needs to be captured in a
293
+ # local variable.
294
+ def capture_return?
295
+ !@constructor &&
296
+ @wrapped.use_return? || returns_return_val?
51
297
  end
52
298
 
53
- def signature
54
- "#{@spec['name']}( #{FunctionSpec.param_list @spec} )"
299
+ # True if the provided wrapped param spec can be cast to when used in this
300
+ # function.
301
+ def castable?(wrapped_param)
302
+ param = @params.find { |p| p.name == wrapped_param['value'] }
303
+
304
+ !param.nil? &&
305
+ !wrapped_param['type'].nil? &&
306
+ @owner.type?(param.type)
55
307
  end
56
308
 
57
- def declaration
58
- modifier_prefix = @spec['static'] ? 'static ' : ''
59
- "#{modifier_prefix}#{@spec['return']['type']} #{signature}"
309
+ # Raises an exception if this function cannot be defined as is. Returns
310
+ # true otherwise.
311
+ def definable_check
312
+ if @wrapped.nil?
313
+ raise UndefinableSpec, 'no wrapped function was specified'
314
+ end
315
+
316
+ true
60
317
  end
61
318
 
62
- def definition(class_name)
63
- return_type = @spec['return']['type']
64
- yield "#{return_type} #{class_name}::#{signature} {"
319
+ # Yields a declaration of each local variable used by the function.
320
+ def locals
321
+ yield 'va_list variadic_args;' if variadic?
65
322
 
66
- wrapped_call = String.new
67
- wrapped_call << "return #{return_type} ( " unless return_type == 'void'
68
- wrapped_call << @owner.function_call(@spec['wrapped-function'])
69
- wrapped_call << ' )' unless return_type == 'void'
70
- yield " #{wrapped_call};"
71
- yield '}'
323
+ if capture_return?
324
+ wrapped_type = resolve_type(@wrapped.return_val_type)
325
+ yield "#{wrapped_type.variable('return_val')};"
326
+ end
327
+ end
328
+
329
+ # The resolved type of the return type.
330
+ def resolved_return
331
+ @return_type.resolve(self)
332
+ end
333
+
334
+ # The function to use to create the return value of the function.
335
+ def return_cast(value)
336
+ if @return_type == @wrapped.return_val_type
337
+ value
338
+ elsif @spec['return']['overloaded']
339
+ "new#{@spec['return']['type'].chomp('*').strip} ( #{value} )"
340
+ else
341
+ resolved_return.cast_expression(value)
342
+ end
343
+ end
344
+
345
+ # The name of the variable holding the return value.
346
+ def return_variable
347
+ if @constructor
348
+ 'this->equivalent'
349
+ else
350
+ 'return_val'
351
+ end
352
+ end
353
+
354
+ # True if the function returns the result of the wrapped function call
355
+ # directly without any after actions.
356
+ def returns_call_directly?
357
+ !@constructor &&
358
+ !@destructor &&
359
+ !%w[void self-reference].include?(@spec['return']['type']) &&
360
+ !@wrapped.error_check?
361
+ end
362
+
363
+ # True if the function returns the return_val variable.
364
+ def returns_return_val?
365
+ !@return_type.self? &&
366
+ @spec['return']['type'] != 'void' &&
367
+ !returns_call_directly?
368
+ end
369
+
370
+ # The expression containing the call to the underlying wrapped function.
371
+ def wrapped_call_expression
372
+ call = @wrapped.call_from(self)
373
+
374
+ if @constructor
375
+ "this->equivalent = #{call}"
376
+ elsif @wrapped.error_check?
377
+ "return_val = #{call}"
378
+ elsif returns_call_directly?
379
+ "return #{return_cast(call)}"
380
+ else
381
+ call
382
+ end
72
383
  end
73
384
  end
74
385
  end
@@ -0,0 +1,62 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ # frozen_string_literal: true
4
+
5
+ #--
6
+ # Copyright 2019 Joel E. Anderson
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+ #++
20
+
21
+ module Wrapture
22
+ # Normalizes a spec key to be boolean, raising an error if it is not. Keys
23
+ # that are not present are defaulted to false.
24
+ def self.normalize_boolean(spec, key)
25
+ is_boolean = [true, false].include?(spec[key])
26
+ error_msg = "'#{key}' key may only be true or false"
27
+ raise(InvalidSpecKey, error_msg) unless !spec.key?(key) || is_boolean
28
+
29
+ spec.key?(key) && spec[key]
30
+ end
31
+
32
+ # Normalizes an include list for an element. A single string will be converted
33
+ # into an array containing the single string, and a nil will be converted to
34
+ # an empty array.
35
+ def self.normalize_includes(includes)
36
+ if includes.nil?
37
+ []
38
+ elsif includes.is_a? String
39
+ [includes]
40
+ else
41
+ includes.uniq
42
+ end
43
+ end
44
+
45
+ # Returns the spec version for the provided spec. If the version is not
46
+ # provided in the spec, the newest version that the spec is compliant with
47
+ # will be returned instead.
48
+ #
49
+ # If this spec uses a version unsupported by this version of Wrapture or the
50
+ # spec is otherwise invalid, an exception is raised.
51
+ def self.spec_version(spec)
52
+ if spec.key?('version') && !Wrapture.supports_version?(spec['version'])
53
+ raise UnsupportedSpecVersion
54
+ end
55
+
56
+ if spec.key?('version')
57
+ spec['version']
58
+ else
59
+ Wrapture::VERSION
60
+ end
61
+ end
62
+ end