Class: FFIGen

Inherits:
Object
  • Object
show all
Defined in:
lib/ffi_gen.rb,
lib/ffi_gen/java_output.rb,
lib/ffi_gen/ruby_output.rb

Defined Under Namespace

Modules: Clang Classes: Constant, Enum, FunctionOrCallback, Name, StructOrUnion, Writer

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ FFIGen

Returns a new instance of FFIGen.



223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/ffi_gen.rb', line 223

def initialize(options = {})
  @module_name   = options[:module_name] or fail "No module name given."
  @ffi_lib       = options[:ffi_lib] or fail "No FFI library given."
  @headers       = options[:headers] or fail "No headers given."
  @cflags        = options.fetch :cflags, []
  @prefixes      = options.fetch :prefixes, []
  @blocking      = options.fetch :blocking, []
  @ffi_lib_flags = options.fetch :ffi_lib_flags, nil
  @output        = options.fetch :output, $stdout
  
  @translation_unit = nil
  @declarations = nil
end

Instance Attribute Details

#cflagsObject (readonly)

Returns the value of attribute cflags.



221
222
223
# File 'lib/ffi_gen.rb', line 221

def cflags
  @cflags
end

#ffi_libObject (readonly)

Returns the value of attribute ffi_lib.



221
222
223
# File 'lib/ffi_gen.rb', line 221

def ffi_lib
  @ffi_lib
end

#headersObject (readonly)

Returns the value of attribute headers.



221
222
223
# File 'lib/ffi_gen.rb', line 221

def headers
  @headers
end

#module_nameObject (readonly)

Returns the value of attribute module_name.



221
222
223
# File 'lib/ffi_gen.rb', line 221

def module_name
  @module_name
end

#outputObject (readonly)

Returns the value of attribute output.



221
222
223
# File 'lib/ffi_gen.rb', line 221

def output
  @output
end

#prefixesObject (readonly)

Returns the value of attribute prefixes.



221
222
223
# File 'lib/ffi_gen.rb', line 221

def prefixes
  @prefixes
end

Class Method Details

.generate(options = {}) ⇒ Object



474
475
476
# File 'lib/ffi_gen.rb', line 474

def self.generate(options = {})
  self.new(options).generate
end

Instance Method Details

#declarationsObject



270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/ffi_gen.rb', line 270

def declarations
  return @declarations unless @declarations.nil?
  
  header_files = []
  Clang.get_inclusions translation_unit, proc { |included_file, inclusion_stack, include_length, client_data|
    filename = Clang.get_file_name(included_file).to_s_and_dispose
    header_files << included_file if @headers.any? { |header| header.is_a?(Regexp) ? header =~ filename : filename.end_with?(header) }
  }, nil
  
  @declarations = {}
  unit_cursor = Clang.get_translation_unit_cursor translation_unit
  previous_declaration_end = Clang.get_cursor_location unit_cursor
  Clang.get_children(unit_cursor).each do |declaration|
    file = Clang.get_spelling_location_data(Clang.get_cursor_location(declaration))[:file]
    
    extent = Clang.get_cursor_extent declaration
    comment_range = Clang.get_range previous_declaration_end, Clang.get_range_start(extent)
    unless [:enum_decl, :struct_decl, :union_decl].include? declaration[:kind] # keep comment for typedef_decl
      previous_declaration_end = Clang.get_range_end extent
    end 
    
    next if not header_files.include? file
    
    comment = extract_comment translation_unit, comment_range
    
    read_named_declaration declaration, comment
  end

  @declarations
end

#extract_comment(translation_unit, range, search_backwards = true, return_spelling = true) ⇒ Object



463
464
465
466
467
468
469
470
471
472
# File 'lib/ffi_gen.rb', line 463

def extract_comment(translation_unit, range, search_backwards = true, return_spelling = true)
  tokens = Clang.get_tokens translation_unit, range
  iterator = search_backwards ? tokens.reverse_each : tokens.each
  iterator.each do |token|
    if Clang.get_token_kind(token) == :comment
      return return_spelling ? Clang.get_token_spelling(translation_unit, token).to_s_and_dispose : token
    end
  end
  ""
end

#generateObject



237
238
239
240
241
242
243
244
245
# File 'lib/ffi_gen.rb', line 237

def generate
  code = send "generate_#{File.extname(@output)[1..-1]}"
  if @output.is_a? String
    File.open(@output, "w") { |file| file.write code }
    puts "ffi_gen: #{@output}"
  else
    @output.write code
  end
end

#generate_javaObject



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/ffi_gen/java_output.rb', line 2

def generate_java
  writer = Writer.new "    ", " * ", "/**", " */"
  writer.puts "// Generated by ffi_gen. Please do not change this file by hand.", "import java.util.*;", "import com.sun.jna.*;", "import java.lang.annotation.*;", "import java.lang.reflect.Method;", "", "public interface #{@module_name} extends Library {"
  writer.indent do
    writer.puts "", *IO.readlines(File.join(File.dirname(__FILE__), "java_pre.java")).map(&:rstrip)
    writer.puts "", "public static #{@module_name} INSTANCE = JnaInstanceCreator.createInstance();", ""
    writer.puts "static class JnaInstanceCreator {"
    writer.indent do
      writer.puts "private static #{@module_name} createInstance() {"
      writer.indent do
        writer.puts "DefaultTypeMapper typeMapper = new DefaultTypeMapper();", "typeMapper.addFromNativeConverter(NativeEnum.class, new EnumConverter());", "typeMapper.addToNativeConverter(NativeEnum.class, new EnumConverter());", ""
        writer.puts "Map<String, Object> options = new HashMap<String, Object>();", "options.put(Library.OPTION_FUNCTION_MAPPER, new NativeNameAnnotationFunctionMapper());", "options.put(Library.OPTION_TYPE_MAPPER, typeMapper);", ""
        writer.puts "return (#{@module_name}) Native.loadLibrary(\"#{@ffi_lib}\", #{@module_name}.class, options);"
      end
      writer.puts "}"
    end
    writer.puts "}", ""
    declarations.values.compact.uniq.each do |declaration|
      declaration.write_java writer
    end
  end
  writer.puts "}"
  writer.output
end

#generate_rbObject



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# File 'lib/ffi_gen/ruby_output.rb', line 2

def generate_rb
  writer = Writer.new "  ", "# "
  writer.puts "# Generated by ffi_gen. Please do not change this file by hand.", "", "require 'ffi'", "", "module #{@module_name}"
  writer.indent do
    writer.puts "extend FFI::Library"
    writer.puts "ffi_lib_flags #{@ffi_lib_flags.map(&:inspect).join(', ')}" if @ffi_lib_flags
    writer.puts "ffi_lib '#{@ffi_lib}'", ""
    writer.puts "def self.attach_function(name, *_)", "  begin; super; rescue FFI::NotFoundError => e", "    (class << self; self; end).class_eval { define_method(name) { |*_| raise e } }", "  end", "end", ""
    declarations.values.compact.uniq.each do |declaration|
      declaration.write_ruby writer
    end
  end
  writer.puts "end"
  writer.output
end

#get_pointee_declaration(type) ⇒ Object



455
456
457
458
459
460
461
# File 'lib/ffi_gen.rb', line 455

def get_pointee_declaration(type)
  canonical_type = Clang.get_canonical_type type
  return nil if canonical_type[:kind] != :pointer
  pointee_type = Clang.get_pointee_type canonical_type
  return nil if pointee_type[:kind] != :record
  @declarations[Clang.get_cursor_type(Clang.get_type_declaration(pointee_type))]
end

#read_named_declaration(declaration, comment) ⇒ Object



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# File 'lib/ffi_gen.rb', line 301

def read_named_declaration(declaration, comment)
  name = Name.new self, Clang.get_cursor_spelling(declaration).to_s_and_dispose

  case declaration[:kind]
  when :enum_decl
    enum = Enum.new self, name, comment
    @declarations[Clang.get_cursor_type(declaration)] = enum
    
    previous_constant_location = Clang.get_cursor_location declaration
    next_constant_value = 0
    Clang.get_children(declaration).each do |enum_constant|
      constant_name = Name.new self, Clang.get_cursor_spelling(enum_constant).to_s_and_dispose
      
      constant_location = Clang.get_cursor_location enum_constant
      constant_comment_range = Clang.get_range previous_constant_location, constant_location
      constant_comment = extract_comment translation_unit, constant_comment_range
      previous_constant_location = constant_location
      
      catch :unsupported_value do
        value_cursor = Clang.get_children(enum_constant).first
        constant_value = if value_cursor
          read_value value_cursor
        else
          next_constant_value
        end
        
        enum.constants << { name: constant_name, value: constant_value, comment: constant_comment }
        next_constant_value = constant_value + 1
      end
    end
    
  when :struct_decl, :union_decl
    struct = @declarations.delete(Clang.get_cursor_type(declaration)) || StructOrUnion.new(self, name, (declaration[:kind] == :union_decl))
    raise if not struct.fields.empty?
    struct.comment << "\n#{comment}"
    
    struct_children = Clang.get_children declaration
    previous_field_end = Clang.get_cursor_location declaration
    until struct_children.empty?
      nested_declaration = [:struct_decl, :union_decl].include?(struct_children.first[:kind]) ? struct_children.shift : nil
      field = struct_children.shift
      raise if field[:kind] != :field_decl
      
      field_name = Name.new self, Clang.get_cursor_spelling(field).to_s_and_dispose
      field_extent = Clang.get_cursor_extent field
      
      field_comment_range = Clang.get_range previous_field_end, Clang.get_range_start(field_extent)
      field_comment = extract_comment translation_unit, field_comment_range
      
      # check for comment starting on same line
      next_field_start = struct_children.first ? Clang.get_cursor_location(struct_children.first) : Clang.get_range_end(Clang.get_cursor_extent(declaration))
      following_comment_range = Clang.get_range Clang.get_range_end(field_extent), next_field_start
      following_comment_token = extract_comment translation_unit, following_comment_range, false, false
      if following_comment_token and Clang.get_spelling_location_data(Clang.get_token_location(translation_unit, following_comment_token))[:line] == Clang.get_spelling_location_data(Clang.get_range_end(field_extent))[:line]
        field_comment = Clang.get_token_spelling(translation_unit, following_comment_token).to_s_and_dispose
        previous_field_end = Clang.get_range_end Clang.get_token_extent(translation_unit, following_comment_token)
      else
        previous_field_end = Clang.get_range_end field_extent
      end
      
      if nested_declaration
        read_named_declaration nested_declaration, ""
        decl = @declarations[Clang.get_cursor_type(nested_declaration)]
        decl.name = Name.new(self, name.parts + field_name.parts) if decl and decl.name.empty?
      end
      
      field_type = Clang.get_cursor_type field
      struct.fields << { name: field_name, type: field_type, comment: field_comment }
    end
    
    @declarations[Clang.get_cursor_type(declaration)] = struct
  
  when :function_decl
    function = FunctionOrCallback.new self, name, false, @blocking.include?(name.raw), comment
    function.return_type = Clang.get_cursor_result_type declaration
    @declarations[declaration] = function
    
    Clang.get_children(declaration).each do |function_child|
      next if function_child[:kind] != :parm_decl
      param_name = Name.new self, Clang.get_cursor_spelling(function_child).to_s_and_dispose
      param_type = Clang.get_cursor_type function_child
      tokens = Clang.get_tokens translation_unit, Clang.get_cursor_extent(function_child)
      is_array = tokens.any? { |t| Clang.get_token_spelling(translation_unit, t).to_s_and_dispose == "[" }
      function.parameters << { name: param_name, type: param_type, is_array: is_array }
    end
    
    pointee_declaration = function.parameters.first && get_pointee_declaration(function.parameters.first[:type])
    if pointee_declaration
      type_prefix = pointee_declaration.name.parts.join.downcase
      function_name_parts = name.parts.dup
      while type_prefix.start_with? function_name_parts.first.downcase
        type_prefix = type_prefix[function_name_parts.first.size..-1]
        function_name_parts.shift
      end
      if type_prefix.empty?
        pointee_declaration.oo_functions << [Name.new(self, function_name_parts), function, get_pointee_declaration(function.return_type)]
      end
    end
  
  when :typedef_decl
    typedef_children = Clang.get_children declaration
    if typedef_children.size == 1
      child_declaration = @declarations[Clang.get_cursor_type(typedef_children.first)]
      child_declaration.name = name if child_declaration and child_declaration.name.empty?
      
    elsif typedef_children.size > 1
      callback = FunctionOrCallback.new self, name, true, false, comment
      callback.return_type = Clang.get_cursor_type typedef_children.first
      @declarations[Clang.get_cursor_type(declaration)] = callback
      
      typedef_children[1..-1].each do |param_decl|
        param_name = Name.new self, Clang.get_cursor_spelling(param_decl).to_s_and_dispose
        param_type = Clang.get_cursor_type param_decl
        callback.parameters << { name:param_name, type: param_type }
      end
    end
      
  when :macro_definition
    tokens = Clang.get_tokens translation_unit, Clang.get_cursor_extent(declaration)
    if tokens.size == 3
      if Clang.get_token_kind(tokens[1]) == :literal
        value = Clang.get_token_spelling(translation_unit, tokens[1]).to_s_and_dispose
        value.sub!(/[A-Za-z]+$/, '') unless value.start_with? '0x' # remove number suffixes
        @declarations[name] ||= Constant.new self, name, value
      end 
    end
    
  end
end

#read_value(cursor) ⇒ Object



431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
# File 'lib/ffi_gen.rb', line 431

def read_value(cursor)
  parts = []
  tokens = Clang.get_tokens translation_unit, Clang.get_cursor_extent(cursor)
  tokens.each do |token|
    spelling = Clang.get_token_spelling(translation_unit, token).to_s_and_dispose
    case Clang.get_token_kind(token)
    when :literal
      parts << spelling
    when :punctuation
      case spelling
      when ",", "}"
        # ignored
      when "+", "-", "<<", ">>"
        parts << spelling
      else
        throw :unsupported_value
      end
    else
      throw :unsupported_value
    end
  end
  eval parts.join
end

#to_java_type(full_type, is_array = false) ⇒ Object



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/ffi_gen/java_output.rb', line 27

def to_java_type(full_type, is_array = false)
  canonical_type = Clang.get_canonical_type full_type
  data_array = case canonical_type[:kind]
  when :void            then ["void",       "nil"]
  when :bool            then ["boolean",    "Boolean"]
  when :u_char          then ["byte",       "Integer"]
  when :u_short         then ["short",      "Integer"]
  when :u_int           then ["int",        "Integer"]
  when :u_long          then ["NativeLong", "Integer"]
  when :u_long_long     then ["long",       "Integer"]
  when :char_s, :s_char then ["byte",       "Integer"]
  when :short           then ["short",      "Integer"]
  when :int             then ["int",        "Integer"]
  when :long            then ["NativeLong", "Integer"]
  when :long_long       then ["long",       "Integer"]
  when :float           then ["float",      "Float"]
  when :double          then ["double",     "Float"]
  when :pointer
    if is_array
      element_type = to_java_type Clang.get_pointee_type(canonical_type)
      return { jna_type: "#{element_type[:jna_type]}[]", description: "Array of #{element_type[:description]}", parameter_name: element_type[:parameter_name] }
    end
    
    pointee_type = Clang.get_pointee_type canonical_type
    result = nil
    case pointee_type[:kind]
    when :char_s
      result = ["String", "String"]
    when :record
      pointee_declaration = @declarations[Clang.get_cursor_type(Clang.get_type_declaration(pointee_type))]
      result = [pointee_declaration.java_name, pointee_declaration.java_name] if pointee_declaration and pointee_declaration.written
    when :function_proto
      declaration = @declarations[full_type]
      result = [":#{declaration.java_name}", "Proc(_callback_#{declaration.java_name}_)"] if declaration
    end
    
    if result.nil?
      pointer_depth = 0
      pointer_target_name = ""
      current_type = full_type
      loop do
        declaration = Clang.get_type_declaration current_type
        pointer_target_name = Name.new self, Clang.get_cursor_spelling(declaration).to_s_and_dispose
        break if not pointer_target_name.empty?

        case current_type[:kind]
        when :pointer
          pointer_depth += 1
          current_type = Clang.get_pointee_type current_type
        when :unexposed
          break
        else
          pointer_target_name = Name.new self, Clang.get_type_kind_spelling(current_type[:kind]).to_s_and_dispose
          break
        end
      end
      result = ["Pointer", "FFI::Pointer(#{'*' * pointer_depth}#{pointer_target_name.to_java_classname})", pointer_target_name]
    end
    
    result
  when :record
    declaration = @declarations[canonical_type]
    declaration ? ["#{declaration.java_name}.by_value", declaration.java_name] : ["byte", "unknown"] # TODO
  when :enum
    declaration = @declarations[canonical_type]
    declaration ? [declaration.java_name, "Symbol from _enum_#{declaration.java_name}_", declaration.name] : ["byte", "unknown"] # TODO
  when :constant_array
    element_type_data = to_java_type Clang.get_array_element_type(canonical_type)
    size = Clang.get_array_size canonical_type
    ["[#{element_type_data[:jna_type]}, #{size}]", "Array<#{element_type_data[:description]}>"]
  else
    raise NotImplementedError, "No translation for values of type #{canonical_type[:kind]}"
  end
  
  { jna_type: data_array[0], description: data_array[1], parameter_name: (data_array[2] || Name.new(self, data_array[1])).to_java_downcase }
end

#to_ruby_type(full_type) ⇒ Object



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/ffi_gen/ruby_output.rb', line 18

def to_ruby_type(full_type)
  canonical_type = Clang.get_canonical_type full_type
  data_array = case canonical_type[:kind]
  when :void            then [":void",       "nil"]
  when :bool            then [":bool",       "Boolean"]
  when :u_char          then [":uchar",      "Integer"]
  when :u_short         then [":ushort",     "Integer"]
  when :u_int           then [":uint",       "Integer"]
  when :u_long          then [":ulong",      "Integer"]
  when :u_long_long     then [":ulong_long", "Integer"]
  when :char_s, :s_char then [":char",       "Integer"]
  when :short           then [":short",      "Integer"]
  when :int             then [":int",        "Integer"]
  when :long            then [":long",       "Integer"]
  when :long_long       then [":long_long",  "Integer"]
  when :float           then [":float",      "Float"]
  when :double          then [":double",     "Float"]
  when :pointer
    pointee_type = Clang.get_pointee_type canonical_type
    result = nil
    case pointee_type[:kind]
    when :char_s
      result = [":string", "String"]
    when :record
      pointee_declaration = @declarations[Clang.get_cursor_type(Clang.get_type_declaration(pointee_type))]
      result = [pointee_declaration.ruby_name, pointee_declaration.ruby_name] if pointee_declaration and pointee_declaration.written
    when :function_proto
      declaration = @declarations[full_type]
      result = [":#{declaration.ruby_name}", "Proc(_callback_#{declaration.ruby_name}_)"] if declaration
    end
    
    if result.nil?
      pointer_depth = 0
      pointer_target_name = ""
      current_type = full_type
      loop do
        declaration = Clang.get_type_declaration current_type
        pointer_target_name = Name.new self, Clang.get_cursor_spelling(declaration).to_s_and_dispose
        break if not pointer_target_name.empty?

        case current_type[:kind]
        when :pointer
          pointer_depth += 1
          current_type = Clang.get_pointee_type current_type
        when :unexposed
          break
        else
          pointer_target_name = Name.new self, Clang.get_type_kind_spelling(current_type[:kind]).to_s_and_dispose
          break
        end
      end
      result = [":pointer", "FFI::Pointer(#{'*' * pointer_depth}#{pointer_target_name.to_ruby_classname})", pointer_target_name]
    end
    
    result
  when :record
    declaration = @declarations[canonical_type]
    declaration ? ["#{declaration.ruby_name}.by_value", declaration.ruby_name] : [":char", "unknown"] # TODO
  when :enum
    declaration = @declarations[canonical_type]
    declaration ? [":#{declaration.ruby_name}", "Symbol from _enum_#{declaration.ruby_name}_", declaration.name] : [":char", "unknown"] # TODO
  when :constant_array
    element_type_data = to_ruby_type Clang.get_array_element_type(canonical_type)
    size = Clang.get_array_size canonical_type
    ["[#{element_type_data[:ffi_type]}, #{size}]", "Array<#{element_type_data[:description]}>"]
  when :unexposed
    [":char", "unexposed"]
  else
    raise NotImplementedError, "No translation for values of type #{canonical_type[:kind]}"
  end
  
  { ffi_type: data_array[0], description: data_array[1], parameter_name: (data_array[2] || Name.new(self, data_array[1])).to_ruby_downcase }
end

#translation_unitObject



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/ffi_gen.rb', line 247

def translation_unit
  return @translation_unit unless @translation_unit.nil?
  
  args = []
  @headers.each do |header|
    args.push "-include", header unless header.is_a? Regexp
  end
  args.concat @cflags
  args_ptr = FFI::MemoryPointer.new :pointer, args.size
  pointers = args.map { |arg| FFI::MemoryPointer.from_string arg }
  args_ptr.write_array_of_pointer pointers
  
  index = Clang.create_index 0, 0
  @translation_unit = Clang.parse_translation_unit index, File.join(File.dirname(__FILE__), "ffi_gen/empty.h"), args_ptr, args.size, nil, 0, Clang.enum_type(:translation_unit_flags)[:detailed_preprocessing_record]
  
  Clang.get_num_diagnostics(@translation_unit).times do |i|
    diag = Clang.get_diagnostic @translation_unit, i
    $stderr.puts Clang.format_diagnostic(diag, Clang.default_diagnostic_display_options).to_s_and_dispose
  end
  
  @translation_unit
end