ffi_gen 0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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