ffi_gen 0.7

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,471 @@
1
+ require "ffi_gen/clang"
2
+
3
+ class << Clang
4
+ def get_children(declaration)
5
+ children = []
6
+ visit_children declaration, lambda { |child, child_parent, child_client_data|
7
+ children << child
8
+ :continue
9
+ }, nil
10
+ children
11
+ end
12
+ end
13
+
14
+ class Clang::String
15
+ def to_s
16
+ Clang.get_c_string self
17
+ end
18
+
19
+ def to_s_and_dispose
20
+ str = to_s
21
+ Clang.dispose_string self
22
+ str
23
+ end
24
+ end
25
+
26
+ class FFIGen
27
+ class Enum
28
+ attr_reader :constants
29
+
30
+ def initialize(generator, name, comment)
31
+ @generator = generator
32
+ @name = name
33
+ @comment = comment
34
+ @constants = []
35
+ end
36
+
37
+ def to_s
38
+ prefix_length = 0
39
+ suffix_length = 0
40
+
41
+ unless @constants.size < 2
42
+ search_pattern = @constants.all? { |constant| constant[0].include? "_" } ? /(?<=_)/ : /[A-Z]/
43
+ first_name = @constants.first[0]
44
+
45
+ loop do
46
+ position = first_name.index(search_pattern, prefix_length + 1) or break
47
+ prefix = first_name[0...position]
48
+ break if not @constants.all? { |constant| constant[0].start_with? prefix }
49
+ prefix_length = position
50
+ end
51
+
52
+ loop do
53
+ position = first_name.rindex(search_pattern, first_name.size - suffix_length - 1) or break
54
+ prefix = first_name[position..-1]
55
+ break if not @constants.all? { |constant| constant[0].end_with? prefix }
56
+ suffix_length = first_name.size - position
57
+ end
58
+ end
59
+
60
+ symbols = []
61
+ definitions = []
62
+ symbol_descriptions = []
63
+ @constants.map do |(constant_name, constant_value, constant_comment)|
64
+ symbol = ":#{@generator.to_ruby_lowercase constant_name[prefix_length..(-1 - suffix_length)]}"
65
+ symbols << symbol
66
+ definitions << " #{symbol}#{constant_value ? ", #{constant_value}" : ""}"
67
+ description = constant_comment.split("\n").map { |line| @generator.prepare_comment_line line }
68
+ symbol_descriptions << " # #{symbol}::\n # #{@generator.create_description_comment(description, ' # ', true)}"
69
+ end
70
+
71
+ enum_description = []
72
+ @comment.split("\n").map do |line|
73
+ enum_description << @generator.prepare_comment_line(line)
74
+ end
75
+
76
+ str = ""
77
+ str << @generator.create_description_comment(enum_description, ' # ')
78
+ str << " # === Options:\n#{symbol_descriptions.join("\n")}\n #\n"
79
+ str << " # @return [Array of Symbols]\n"
80
+ str << " def self.#{@generator.to_ruby_lowercase @name}_enum\n [#{symbols.join(', ')}]\n end\n"
81
+ str << " enum :#{@generator.to_ruby_lowercase @name}, [\n#{definitions.join(",\n")}\n ]"
82
+ str
83
+ end
84
+
85
+ def type_name
86
+ "Symbol from #{@generator.to_ruby_lowercase @name}_enum"
87
+ end
88
+
89
+ def reference
90
+ ":#{@generator.to_ruby_lowercase @name}"
91
+ end
92
+ end
93
+
94
+ class Struct
95
+ attr_reader :fields
96
+
97
+ def initialize(generator, name)
98
+ @generator = generator
99
+ @name = name
100
+ @fields = []
101
+ end
102
+
103
+ def to_s
104
+ lines = @fields.map { |(field_name, field_type)| ":#{@generator.to_ruby_lowercase field_name}, #{@generator.to_ffi_type field_type}" }
105
+ " class #{@generator.to_ruby_camelcase @name} < FFI::Struct\n layout #{lines.join(",\n ")}\n end"
106
+ end
107
+
108
+ def type_name
109
+ @generator.to_ruby_camelcase @name
110
+ end
111
+
112
+ def reference
113
+ "#{type_name}.by_value"
114
+ end
115
+ end
116
+
117
+ class Function
118
+ attr_reader :name, :parameters
119
+ attr_accessor :return_type
120
+
121
+ def initialize(generator, name, is_callback, comment)
122
+ @generator = generator
123
+ @name = name
124
+ @parameters = []
125
+ @is_callback = is_callback
126
+ @comment = comment
127
+ end
128
+
129
+ def to_s
130
+ str = ""
131
+
132
+ ruby_name = @generator.to_ruby_lowercase @name
133
+ ruby_parameters = @parameters.map do |(name, type)|
134
+ ruby_param_type = @generator.to_type_name type
135
+ ruby_param_name = @generator.to_ruby_lowercase(name.empty? ? ruby_param_type.split.last : name)
136
+ [ruby_param_name, ruby_param_type, []]
137
+ end
138
+
139
+ signature = "[#{@parameters.map{ |(name, type)| @generator.to_ffi_type type }.join(', ')}], #{@generator.to_ffi_type @return_type}"
140
+ if @is_callback
141
+ str << " callback :#{ruby_name}, #{signature}"
142
+ else
143
+ function_description = []
144
+ return_value_description = []
145
+ current_description = function_description
146
+ @comment.split("\n").map do |line|
147
+ line = @generator.prepare_comment_line line
148
+ if line.gsub! /\\param (.*?) /, ''
149
+ index = @parameters.index { |(name, type)| name == $1 }
150
+ if index
151
+ current_description = ruby_parameters[index][2]
152
+ else
153
+ current_description << "#{$1}: "
154
+ end
155
+ end
156
+ current_description = return_value_description if line.gsub! '\\returns ', ''
157
+ current_description << line
158
+ end
159
+
160
+ str << @generator.create_description_comment(function_description, ' # ')
161
+ str << " # @method #{ruby_name}(#{ruby_parameters.map{ |(name, type, description)| name }.join(', ')})\n"
162
+ ruby_parameters.each do |(name, type, description)|
163
+ str << " # @param [#{type}] #{name} #{@generator.create_description_comment(description, ' # ', true)}\n"
164
+ end
165
+ str << " # @return [#{@generator.to_type_name @return_type}] #{@generator.create_description_comment(return_value_description, ' # ', true)}\n"
166
+ str << " # @scope class\n"
167
+ str << " attach_function :#{ruby_name}, :#{@name}, #{signature}"
168
+ end
169
+ str
170
+ end
171
+
172
+ def type_name
173
+ "Callback"
174
+ end
175
+
176
+ def reference
177
+ ":#{@generator.to_ruby_lowercase @name}"
178
+ end
179
+ end
180
+
181
+ attr_reader :ruby_module, :ffi_lib, :headers, :output, :blacklist, :cflags
182
+
183
+ def initialize(options = {})
184
+ @ruby_module = options[:ruby_module] or fail "No module name given."
185
+ @ffi_lib = options[:ffi_lib] or fail "No FFI library given."
186
+ @headers = options[:headers] or fail "No headers given."
187
+ @cflags = options.fetch :cflags, []
188
+ @prefixes = options.fetch :prefixes, []
189
+ @blacklist = options.fetch :blacklist, []
190
+ @output = options.fetch :output, $stdout
191
+
192
+ @translation_unit = nil
193
+ @declarations = nil
194
+ end
195
+
196
+ def translation_unit
197
+ return @translation_unit unless @translation_unit.nil?
198
+
199
+ args = []
200
+ @headers.each do |header|
201
+ args.push "-include", header
202
+ end
203
+ args.concat @cflags
204
+ args_ptr = FFI::MemoryPointer.new :pointer, args.size
205
+ pointers = args.map { |arg| FFI::MemoryPointer.from_string arg }
206
+ args_ptr.write_array_of_pointer pointers
207
+
208
+ index = Clang.create_index 0, 0
209
+ @translation_unit = Clang.parse_translation_unit index, File.join(File.dirname(__FILE__), "ffi_gen/empty.h"), args_ptr, args.size, nil, 0, 0
210
+
211
+ Clang.get_num_diagnostics(@translation_unit).times do |i|
212
+ diag = Clang.get_diagnostic @translation_unit, i
213
+ $stderr.puts Clang.format_diagnostic(diag, Clang.default_diagnostic_display_options).to_s_and_dispose
214
+ end
215
+
216
+ @translation_unit
217
+ end
218
+
219
+ def declarations
220
+ return @declarations unless @declarations.nil?
221
+
222
+ header_files = []
223
+ Clang.get_inclusions translation_unit, proc { |included_file, inclusion_stack, include_length, client_data|
224
+ filename = Clang.get_file_name(included_file).to_s_and_dispose
225
+ header_files << included_file if @headers.any? { |header| filename.end_with? header }
226
+ }, nil
227
+
228
+ @declarations = {}
229
+ unit_cursor = Clang.get_translation_unit_cursor translation_unit
230
+ previous_declaration_end = Clang.get_cursor_location unit_cursor
231
+ Clang.get_children(unit_cursor).each do |declaration|
232
+ file_ptr = FFI::MemoryPointer.new :pointer
233
+ Clang.get_spelling_location Clang.get_cursor_location(declaration), file_ptr, nil, nil, nil
234
+ file = file_ptr.read_pointer
235
+
236
+ extent = Clang.get_cursor_extent declaration
237
+ comment_range = Clang.get_range previous_declaration_end, Clang.get_range_start(extent)
238
+ previous_declaration_end = Clang.get_range_end extent
239
+
240
+ next if not header_files.include? file
241
+
242
+ name = Clang.get_cursor_spelling(declaration).to_s_and_dispose
243
+ next if blacklist.include? name
244
+
245
+ comment = extract_comment translation_unit, comment_range
246
+
247
+ case declaration[:kind]
248
+ when :enum_decl
249
+ read_named_declaration declaration, name, comment unless name.empty?
250
+
251
+ when :function_decl
252
+ function = Function.new self, name, false, comment
253
+ function.return_type = Clang.get_cursor_result_type declaration
254
+ @declarations[name] = function
255
+
256
+ Clang.get_children(declaration).each do |function_child|
257
+ next if function_child[:kind] != :parm_decl
258
+ param_name = Clang.get_cursor_spelling(function_child).to_s_and_dispose
259
+ param_type = Clang.get_cursor_type function_child
260
+ function.parameters << [param_name, param_type]
261
+ end
262
+
263
+ when :typedef_decl
264
+ typedef_children = Clang.get_children declaration
265
+ if typedef_children.size == 1
266
+ read_named_declaration typedef_children.first, name, comment unless @declarations.has_key? name
267
+
268
+ elsif typedef_children.size > 1
269
+ callback = Function.new self, name, true, comment
270
+ callback.return_type = Clang.get_cursor_type typedef_children.first
271
+ @declarations[name] = callback
272
+
273
+ typedef_children[1..-1].each do |param_decl|
274
+ param_name = Clang.get_cursor_spelling(param_decl).to_s_and_dispose
275
+ param_type = Clang.get_cursor_type param_decl
276
+ callback.parameters << [param_name, param_type]
277
+ end
278
+ end
279
+
280
+ end
281
+ end
282
+
283
+ @declarations
284
+ end
285
+
286
+ def generate
287
+ content = "# Generated by ffi_gen. Please do not change this file by hand.\n\nrequire 'ffi'\n\nmodule #{@ruby_module}\n extend FFI::Library\n ffi_lib '#{@ffi_lib}'\n\n#{declarations.values.join("\n\n")}\n\nend"
288
+ if @output.is_a? String
289
+ File.open(@output, "w") { |file| file.write content }
290
+ puts "ffi_gen: #{@output}"
291
+ else
292
+ @output.write content
293
+ end
294
+ end
295
+
296
+ def read_named_declaration(declaration, name, comment)
297
+ case declaration[:kind]
298
+ when :enum_decl
299
+ enum = Enum.new self, name, comment
300
+ @declarations[name] = enum
301
+
302
+ previous_constant_location = Clang.get_cursor_location declaration
303
+ Clang.get_children(declaration).each do |enum_constant|
304
+ constant_name = Clang.get_cursor_spelling(enum_constant).to_s_and_dispose
305
+ constant_location = Clang.get_cursor_location enum_constant
306
+
307
+ constant_value = nil
308
+ value_cursor = Clang.get_children(enum_constant).first
309
+ constant_value = value_cursor && case value_cursor[:kind]
310
+ when :integer_literal
311
+ tokens_ptr_ptr = FFI::MemoryPointer.new :pointer
312
+ num_tokens_ptr = FFI::MemoryPointer.new :uint
313
+ Clang.tokenize translation_unit, Clang.get_cursor_extent(value_cursor), tokens_ptr_ptr, num_tokens_ptr
314
+ token = Clang::Token.new tokens_ptr_ptr.read_pointer
315
+ literal = Clang.get_token_spelling translation_unit, token
316
+ Clang.dispose_tokens translation_unit, tokens_ptr_ptr.read_pointer, num_tokens_ptr.read_uint
317
+ literal
318
+ else
319
+ next # skip those entries for now
320
+ end
321
+
322
+ constant_comment_range = Clang.get_range previous_constant_location, constant_location
323
+ constant_comment = extract_comment translation_unit, constant_comment_range
324
+ previous_constant_location = constant_location
325
+
326
+ enum.constants << [constant_name, constant_value, constant_comment]
327
+ end
328
+
329
+ when :struct_decl
330
+ struct = Struct.new self, name
331
+ @declarations[name] = struct
332
+
333
+ Clang.get_children(declaration).each do |field_decl|
334
+ field_name = Clang.get_cursor_spelling(field_decl).to_s_and_dispose
335
+ field_type = Clang.get_cursor_type field_decl
336
+ struct.fields << [field_name, field_type]
337
+ end
338
+ end
339
+ end
340
+
341
+ def extract_comment(translation_unit, range)
342
+ tokens_ptr_ptr = FFI::MemoryPointer.new :pointer
343
+ num_tokens_ptr = FFI::MemoryPointer.new :uint
344
+ Clang.tokenize translation_unit, range, tokens_ptr_ptr, num_tokens_ptr
345
+ num_tokens = num_tokens_ptr.read_uint
346
+ tokens_ptr = FFI::Pointer.new Clang::Token, tokens_ptr_ptr.read_pointer
347
+ (num_tokens - 1).downto(0) do |i|
348
+ token = Clang::Token.new tokens_ptr[i]
349
+ return Clang.get_token_spelling(translation_unit, token).to_s_and_dispose if Clang.get_token_kind(token) == :comment
350
+ end
351
+ ""
352
+ end
353
+
354
+ def to_ffi_type(full_type)
355
+ declaration = Clang.get_type_declaration full_type
356
+ name = Clang.get_cursor_spelling(declaration).to_s_and_dispose
357
+ return @declarations[name].reference if @declarations.has_key? name
358
+
359
+ canonical_type = Clang.get_canonical_type full_type
360
+ case canonical_type[:kind]
361
+ when :void then ":void"
362
+ when :bool then ":bool"
363
+ when :u_char then ":uchar"
364
+ when :u_short then ":ushort"
365
+ when :u_int then ":uint"
366
+ when :u_long then ":ulong"
367
+ when :u_long_long then ":ulong_long"
368
+ when :short then ":short"
369
+ when :int then ":int"
370
+ when :long then ":long"
371
+ when :long_long then ":long_long"
372
+ when :float then ":float"
373
+ when :double then ":double"
374
+ when :pointer
375
+ pointee_type = Clang.get_pointee_type canonical_type
376
+ pointee_type[:kind] == :char_s ? ":string" : ":pointer"
377
+ when :constant_array
378
+ element_type = Clang.get_array_element_type canonical_type
379
+ size = Clang.get_array_size canonical_type
380
+ "[#{to_ffi_type element_type}, #{size}]"
381
+ else
382
+ raise NotImplementedError, "No translation for values of type #{canonical_type[:kind]}"
383
+ end
384
+ end
385
+
386
+ def to_type_name(full_type)
387
+ declaration = Clang.get_type_declaration full_type
388
+ name = Clang.get_cursor_spelling(declaration).to_s_and_dispose
389
+ return @declarations[name].type_name if @declarations.has_key? name
390
+
391
+ canonical_type = Clang.get_canonical_type full_type
392
+ case canonical_type[:kind]
393
+ when :void then "nil"
394
+ when :bool then "Boolean"
395
+ when :u_char, :u_short, :u_int, :u_long, :u_long_long, :short, :int, :long, :long_long then "Integer"
396
+ when :float, :double then "Float"
397
+ when :pointer
398
+ pointee_type = Clang.get_pointee_type canonical_type
399
+ if pointee_type[:kind] == :char_s
400
+ "String"
401
+ elsif not name.empty?
402
+ "FFI::Pointer of #{to_ruby_camelcase name}"
403
+ else
404
+ pointee_declaration = Clang.get_type_declaration full_type
405
+ pointee_name = Clang.get_cursor_spelling(pointee_declaration).to_s_and_dispose
406
+ "FFI::Pointer to #{pointee_name}"
407
+ end
408
+ else
409
+ raise NotImplementedError, "No type name for type #{canonical_type[:kind]}"
410
+ end
411
+ end
412
+
413
+ def to_ruby_lowercase(str)
414
+ str = str.dup
415
+ str.sub! /^(#{@prefixes.join('|')})/, '' # remove prefixes
416
+ str.gsub! /([A-Z][a-z])/, '_\1' # add underscores before word beginnings
417
+ str.gsub! /([a-z])([A-Z])/, '\1_\2' # add underscores after word endings
418
+ str.sub! /^_*/, '' # remove underscores at the beginning
419
+ str.gsub! /__+/, '_' # replace multiple underscores by only one
420
+ str.downcase!
421
+ str
422
+ end
423
+
424
+ def to_ruby_camelcase(str)
425
+ str = str.dup
426
+ str.sub! /^(#{@prefixes.join('|')})/, '' # remove prefixes
427
+ str
428
+ end
429
+
430
+ def prepare_comment_line(line)
431
+ line = line.dup
432
+ line.sub! /\ ?\*+\/\s*$/, ''
433
+ line.sub! /^\s*\/?\*+ ?/, ''
434
+ line.gsub! /\\(brief|determine) /, ''
435
+ line.gsub! '[', '('
436
+ line.gsub! ']', ')'
437
+ line
438
+ end
439
+
440
+ def create_description_comment(description, line_prefix, inline_mode = false)
441
+ description.shift while not description.empty? and description.first.strip.empty?
442
+ description.pop while not description.empty? and description.last.strip.empty?
443
+ return "" if description.empty?
444
+
445
+ str = ""
446
+ description << "" if not inline_mode # empty line at end
447
+ description.each_with_index do |line, index|
448
+ str << line_prefix if not inline_mode or index > 0
449
+ str << line
450
+ str << "\n" if not inline_mode or index < description.size - 1
451
+ end
452
+ str
453
+ end
454
+
455
+ def self.generate(options = {})
456
+ self.new(options).generate
457
+ end
458
+
459
+ end
460
+
461
+ if __FILE__ == $0
462
+ FFIGen.generate(
463
+ ruby_module: "Clang",
464
+ ffi_lib: "clang",
465
+ headers: ["clang-c/Index.h"],
466
+ cflags: `llvm-config --cflags`.split(" "),
467
+ prefixes: ["clang_", "CX"],
468
+ blacklist: ["clang_getExpansionLocation"],
469
+ output: "ffi_gen/clang.rb"
470
+ )
471
+ end