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: ArrayType, ByValueType, Define, Enum, FunctionOrCallback, Name, PointerType, PrimitiveType, StringType, StructOrUnion, Type, UnknownType, Writer

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ FFIGen

Returns a new instance of FFIGen.



264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/ffi_gen.rb', line 264

def initialize(options = {})
  @module_name   = options[:module_name] or fail "No module name given."
  @ffi_lib       = options.fetch :ffi_lib, nil
  @headers       = options[:headers] or fail "No headers given."
  @cflags        = options.fetch :cflags, []
  @prefixes      = options.fetch :prefixes, []
  @suffixes      = options.fetch :suffixes, []
  @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.



262
263
264
# File 'lib/ffi_gen.rb', line 262

def cflags
  @cflags
end

#ffi_libObject (readonly)

Returns the value of attribute ffi_lib.



262
263
264
# File 'lib/ffi_gen.rb', line 262

def ffi_lib
  @ffi_lib
end

#headersObject (readonly)

Returns the value of attribute headers.



262
263
264
# File 'lib/ffi_gen.rb', line 262

def headers
  @headers
end

#module_nameObject (readonly)

Returns the value of attribute module_name.



262
263
264
# File 'lib/ffi_gen.rb', line 262

def module_name
  @module_name
end

#outputObject (readonly)

Returns the value of attribute output.



262
263
264
# File 'lib/ffi_gen.rb', line 262

def output
  @output
end

#prefixesObject (readonly)

Returns the value of attribute prefixes.



262
263
264
# File 'lib/ffi_gen.rb', line 262

def prefixes
  @prefixes
end

Class Method Details

.generate(options = {}) ⇒ Object



744
745
746
# File 'lib/ffi_gen.rb', line 744

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

Instance Method Details

#declarationsObject



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
# File 'lib/ffi_gen.rb', line 312

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

  unit_cursor = Clang.get_translation_unit_cursor translation_unit
  declaration_cursors = Clang.get_children unit_cursor
  declaration_cursors.delete_if { |cursor| [:macro_expansion, :inclusion_directive, :var_decl].include? cursor[:kind] }
  declaration_cursors.delete_if { |cursor| !header_files.include?(Clang.get_spelling_location_data(Clang.get_cursor_location(cursor))[:file]) }

  is_nested_declaration = []
  min_offset = Clang.get_spelling_location_data(Clang.get_cursor_location(declaration_cursors.last))[:offset]
  declaration_cursors.reverse_each do |declaration_cursor|
    offset = Clang.get_spelling_location_data(Clang.get_cursor_location(declaration_cursor))[:offset]
    is_nested_declaration.unshift(offset > min_offset)
    min_offset = offset if offset < min_offset
  end

  @declarations = []
  @declarations_by_name = {}
  @declarations_by_type = {}
  previous_declaration_end = Clang.get_cursor_location unit_cursor
  declaration_cursors.each_with_index do |declaration_cursor, index|
    comment = []
    unless is_nested_declaration[index]
      comment_range = Clang.get_range previous_declaration_end, Clang.get_cursor_location(declaration_cursor)
      comment, _ = extract_comment translation_unit, comment_range
      previous_declaration_end = Clang.get_range_end Clang.get_cursor_extent(declaration_cursor)
    end

    read_declaration declaration_cursor, comment
  end

  @declarations
end

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



717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
# File 'lib/ffi_gen.rb', line 717

def extract_comment(translation_unit, range, search_backwards = true)
  tokens = Clang.get_tokens translation_unit, range

  iterator = search_backwards ? tokens.reverse_each : tokens.each
  comment_lines = []
  comment_token = nil
  comment_block = false
  iterator.each do |token|
    next if Clang.get_token_kind(token) != :comment
    comment = Clang.get_token_spelling(translation_unit, token).to_s_and_dispose
    lines = comment.split("\n").map { |line|
      line.sub!(/\ ?\*+\/\s*$/, '')
      line.sub!(/^\s*\/?[*\/]+ ?/, '')
      line.gsub!(/\\(brief|determine) /, '')
      line.gsub!('[', '(')
      line.gsub!(']', ')')
      line
    }
    comment_lines = lines + comment_lines
    comment_token = token
    comment_block = !comment_block if comment == "///"
    break unless comment_block and search_backwards
  end

  return comment_lines, comment_token
end

#generateObject



279
280
281
282
283
284
285
286
287
# File 'lib/ffi_gen.rb', line 279

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.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.inspect}", "" if @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.each do |declaration|
      declaration.write_ruby writer
    end
  end
  writer.puts "end"
  writer.output
end

#get_pointee_declaration(type) ⇒ Object



709
710
711
712
713
714
715
# File 'lib/ffi_gen.rb', line 709

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_by_type[Clang.get_cursor_type(Clang.get_type_declaration(pointee_type))]
end

#read_declaration(declaration_cursor, comment) ⇒ Object



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
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
# File 'lib/ffi_gen.rb', line 352

def read_declaration(declaration_cursor, comment)
  name = read_name declaration_cursor

  declaration = case declaration_cursor[:kind]
  when :enum_decl
    enum_description = []
    constant_descriptions = {}
    current_description = enum_description
    comment.each do |line|
      if line.gsub!(/@(.*?): /, '')
        current_description = []
        constant_descriptions[$1] = current_description
      end
      current_description = enum_description if line.strip.empty?
      current_description << line
    end

    constants = []
    previous_constant_location = Clang.get_cursor_location declaration_cursor
    next_constant_value = 0
    Clang.get_children(declaration_cursor).each do |enum_constant|
      constant_name = read_name enum_constant

      constant_location = Clang.get_cursor_location enum_constant
      constant_comment_range = Clang.get_range previous_constant_location, constant_location
      constant_description, _ = extract_comment translation_unit, constant_comment_range
      constant_description.concat(constant_descriptions[constant_name.raw] || [])
      previous_constant_location = constant_location

      begin
        value_cursor = Clang.get_children(enum_constant).first
        constant_value = if value_cursor
          parts = []
          Clang.get_tokens(translation_unit, Clang.get_cursor_extent(value_cursor)).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 "+", "-", "<<", ">>", "(", ")"
                parts << spelling
              else
                raise ArgumentError
              end
            else
              raise ArgumentError
            end
          end
          eval parts.join
        else
          next_constant_value
        end

        constants << { name: constant_name, value: constant_value, comment: constant_description }
        next_constant_value = constant_value + 1
      rescue ArgumentError
        puts "Warning: Could not process value of enum constant \"#{constant_name.raw}\""
      end
    end

    Enum.new self, name, constants, enum_description

  when :struct_decl, :union_decl
    struct = @declarations_by_type[Clang.get_cursor_type(declaration_cursor)] || StructOrUnion.new(self, name, (declaration_cursor[:kind] == :union_decl))
    raise if not struct.fields.empty?
    struct.description.concat comment

    struct_children = Clang.get_children declaration_cursor
    previous_field_end = Clang.get_cursor_location declaration_cursor
    last_nested_declaration = nil
    until struct_children.empty?
      child = struct_children.shift
      case child[:kind]
      when :struct_decl, :union_decl
        last_nested_declaration = read_declaration child, []
      when :field_decl
        field_name = read_name child
        field_extent = Clang.get_cursor_extent child

        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_cursor))
        following_comment_range = Clang.get_range Clang.get_range_end(field_extent), next_field_start
        following_comment, following_comment_token = extract_comment translation_unit, following_comment_range, 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 = following_comment
          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

        field_type = resolve_type Clang.get_cursor_type(child)
        last_nested_declaration.name ||= Name.new(name.parts + field_name.parts) if last_nested_declaration
        last_nested_declaration = nil
        struct.fields << { name: field_name, type: field_type, comment: field_comment }
      else
        raise
      end
    end

    struct

  when :function_decl
    function_description = []
    return_value_description = []
    parameter_descriptions = {}
    current_description = function_description
    comment.each do |line|
      if line.gsub!(/\\param (.*?) /, '')
        current_description = []
        parameter_descriptions[$1] = current_description
      end
      current_description = return_value_description if line.gsub! '\\returns ', ''
      current_description << line
    end

    return_type = resolve_type Clang.get_cursor_result_type(declaration_cursor)
    parameters = []
    first_parameter_type = nil
    Clang.get_children(declaration_cursor).each do |function_child|
      next if function_child[:kind] != :parm_decl
      param_name = read_name 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 == "[" }
      param_type = resolve_type Clang.get_cursor_type(function_child), is_array
      param_name ||= param_type.name
      param_name ||= Name.new []
      first_parameter_type ||= Clang.get_cursor_type function_child
      parameters << { name: param_name, type: param_type }
    end

    parameters.each_with_index do |parameter, index|
      parameter[:description] = parameter[:name] && parameter_descriptions[parameter[:name].raw]
      parameter[:description] ||= parameter_descriptions.values[index] if parameter_descriptions.size == parameters.size # workaround for wrong names
      parameter[:description] ||= []
    end

    function = FunctionOrCallback.new self, name, parameters, return_type, false, @blocking.include?(name.raw), function_description, return_value_description

    pointee_declaration = first_parameter_type && get_pointee_declaration(first_parameter_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
        break if function_name_parts.empty?
      end
      if type_prefix.empty?
        pointee_declaration.oo_functions << [Name.new(function_name_parts), function]
      end
    end

    function

  when :typedef_decl
    typedef_children = Clang.get_children declaration_cursor
    if typedef_children.size == 1
      child_declaration = @declarations_by_type[Clang.get_cursor_type(typedef_children.first)]
      child_declaration.name = name if child_declaration and child_declaration.name.nil?
      nil
    elsif typedef_children.size > 1
      return_type = resolve_type Clang.get_cursor_type(typedef_children.first)
      parameters = []
      typedef_children.each do |param_decl|
        param_name = read_name param_decl
        param_type = resolve_type Clang.get_cursor_type(param_decl)
        param_name ||= param_type.name
        parameters << { name:param_name, type: param_type, description: [] }
      end
      FunctionOrCallback.new self, name, parameters, return_type, true, false, comment, []
    else
      nil
    end

  when :macro_definition
    tokens = Clang.get_tokens(translation_unit, Clang.get_cursor_extent(declaration_cursor)).map { |token|
      [Clang.get_token_kind(token), Clang.get_token_spelling(translation_unit, token).to_s_and_dispose]
    }
    if tokens.size > 1
      tokens.shift
      begin
        parameters = nil
        if tokens.first[1] == "("
          tokens_backup = tokens.dup
          begin
            parameters = []
            tokens.shift
            loop do
              kind, spelling = tokens.shift
              case kind
              when :identifier
                parameters << spelling
              when :punctuation
                break if spelling == ")"
                raise ArgumentError unless spelling == ","
              else
                raise ArgumentError
              end
            end
          rescue ArgumentError
            parameters = nil
            tokens = tokens_backup
          end
        end
        value = []
        until tokens.empty?
          kind, spelling = tokens.shift
          case kind
          when :literal
            value << spelling
          when :punctuation
            case spelling
            when "+", "-", "<<", ">>", ")"
              value << spelling
            when ","
              value << ", "
            when "("
              if tokens[1][1] == ")"
                tokens.delete_at 1
              else
                value << spelling
              end
            else
              raise ArgumentError
            end
          when :identifier
            raise ArgumentError unless parameters
            if parameters.include? spelling
              value << spelling
            elsif spelling == "NULL"
              value << "nil"
            else
              if not tokens.empty? and tokens.first[1] == "("
                tokens.shift
                if spelling == "strlen"
                  argument_kind, argument_spelling = tokens.shift
                  second_token_kind, second_token_spelling = tokens.shift
                  raise ArgumentError unless argument_kind == :identifier and second_token_spelling == ")"
                  value << "#{argument_spelling}.length"
                else
                  value << [:method, read_name(spelling)]
                  value << "("
                end
              else
                value << [:constant, read_name(spelling)]
              end
            end
          when :keyword
            raise ArgumentError unless spelling == "sizeof" and tokens[0][1] == "(" and tokens[1][0] == :literal and tokens[2][1] == ")"
            tokens.shift
            argument_kind, argument_spelling = tokens.shift
            value << "#{argument_spelling}.length"
            tokens.shift
          else
            raise ArgumentError
          end
        end
        Define.new(self, name, parameters, value)
      rescue ArgumentError
        puts "Warning: Could not process value of macro \"#{name.raw}\""
        nil
      end
    else
      nil
    end

  else
    raise declaration_cursor[:kind].to_s

  end

  return nil if declaration.nil?
  @declarations.delete declaration
  @declarations << declaration
  @declarations_by_name[name] = name.raw unless name.nil?
  type = Clang.get_cursor_type declaration_cursor
  @declarations_by_type[type] = declaration unless type.nil?

  declaration
end

#read_name(source) ⇒ Object



700
701
702
703
704
705
706
707
# File 'lib/ffi_gen.rb', line 700

def read_name(source)
  source = Clang.get_cursor_spelling(source).to_s_and_dispose if source.is_a? Clang::Cursor
  return nil if source.empty?
  trimmed = source.sub(/^(#{@prefixes.join('|')})/, '')
  trimmed = trimmed.sub(/(#{@suffixes.join('|')})$/, '')
  parts = trimmed.split(/_|(?=[A-Z][a-z])|(?<=[a-z])(?=[A-Z])/).reject(&:empty?)
  Name.new parts, source
end

#resolve_type(full_type, is_array = false) ⇒ Object



637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
# File 'lib/ffi_gen.rb', line 637

def resolve_type(full_type, is_array = false)
  canonical_type = Clang.get_canonical_type full_type
  data_array = case canonical_type[:kind]
  when :void, :bool, :u_char, :u_short, :u_int, :u_long, :u_long_long, :char_s, :s_char, :short, :int, :long, :long_long, :float, :double
    PrimitiveType.new canonical_type[:kind]
  when :pointer
    if is_array
      ArrayType.new resolve_type(Clang.get_pointee_type(canonical_type)), nil
    else
      pointee_type = Clang.get_pointee_type canonical_type
      type = case pointee_type[:kind]
      when :char_s
        StringType.new
      when :record
        @declarations_by_type[Clang.get_cursor_type(Clang.get_type_declaration(pointee_type))]
      when :function_proto
        @declarations_by_type[full_type]
      else
        nil
      end

      if type.nil?
        pointer_depth = 0
        pointee_name = ""
        current_type = full_type
        loop do
          declaration_cursor = Clang.get_type_declaration current_type
          pointee_name = read_name declaration_cursor
          break if pointee_name

          case current_type[:kind]
          when :pointer
            pointer_depth += 1
            current_type = Clang.get_pointee_type current_type
          when :unexposed
            break
          else
            pointee_name = Name.new Clang.get_type_kind_spelling(current_type[:kind]).to_s_and_dispose.split("_")
            break
          end
        end
        type = PointerType.new pointee_name, pointer_depth
      end

      type
    end
  when :record
    type = @declarations_by_type[canonical_type]
    type &&= ByValueType.new(type)
    type || UnknownType.new # TODO
  when :enum
    @declarations_by_type[canonical_type] || UnknownType.new # TODO
  when :constant_array
    ArrayType.new resolve_type(Clang.get_array_element_type(canonical_type)), Clang.get_array_size(canonical_type)
  when :unexposed, :function_proto
    UnknownType.new
  when :incomplete_array
    PointerType.new resolve_type(Clang.get_array_element_type(canonical_type)).name, 1
  else
    raise NotImplementedError, "No translation for values of type #{canonical_type[:kind]}"
  end
end

#translation_unitObject



289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/ffi_gen.rb', line 289

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