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.
- data/lib/ffi_gen.rb +471 -0
- data/lib/ffi_gen/clang.rb +3485 -0
- data/lib/ffi_gen/empty.h +0 -0
- metadata +60 -0
data/lib/ffi_gen.rb
ADDED
@@ -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
|