class FFIGen

Attributes

cflags[R]
ffi_lib[R]
headers[R]
module_name[R]
output[R]
prefixes[R]

Public Class Methods

generate(options = {}) click to toggle source
# File lib/ffi_gen.rb, line 819
def self.generate(options = {})
  self.new(options).generate
end
new(options = {}) click to toggle source
# File lib/ffi_gen.rb, line 280
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

Public Instance Methods

declarations() click to toggle source
# File lib/ffi_gen.rb, line 328
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) click to toggle source
# File lib/ffi_gen.rb, line 792
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
generate() click to toggle source
# File lib/ffi_gen.rb, line 295
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_java() click to toggle source
# 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 class #{@module_name} {"
  writer.indent do
    writer.puts "private static #{@module_name}Interface INSTANCE = #{@module_name}Interface.JnaInstanceCreator.createInstance();"
    writer.puts "", *IO.readlines(File.join(File.dirname(__FILE__), "java_static.java")).map(&:rstrip), ""

    declarations.each do |declaration|
      if declaration.respond_to? :write_static_java
        declaration.write_static_java writer
      else
        declaration.write_java writer
      end
    end
    writer.puts ""

    writer.puts "interface #{@module_name}Interface extends Library {"
    writer.indent do
      writer.puts "", *IO.readlines(File.join(File.dirname(__FILE__), "java_interface.java")).map(&:rstrip), ""
      writer.puts "static class JnaInstanceCreator {"
      writer.indent do
        writer.puts "private static #{@module_name}Interface 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}Interface) Native.loadLibrary(\"#{@ffi_lib}\", #{@module_name}Interface.class, options);"
        end
        writer.puts "}"
      end
      writer.puts "}", ""
      declarations.each do |declaration|
        if declaration.is_a? FunctionOrCallback
          declaration.write_java writer
        end
      end
    end
    writer.puts "}"
  end
  writer.puts "}"
  writer.output
end
generate_rb() click to toggle source
# 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) click to toggle source
# File lib/ffi_gen.rb, line 784
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) click to toggle source
# File lib/ffi_gen.rb, line 368
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
      next if constant_name.nil?

      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
    @declarations_by_type[Clang.get_cursor_type(declaration_cursor)] ||= StructOrUnion.new(self, name, (declaration_cursor[:kind] == :union_decl))
    struct = @declarations_by_type[Clang.get_cursor_type(declaration_cursor)]
    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 }
      when :unexposed_attr
      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
    underlying = Clang.get_typedef_decl_underlying_type(declaration_cursor)
    function_proto = Clang.get_canonical_type(Clang.get_pointee_type(underlying))
    if function_proto[:kind] == :function_proto || function_proto[:kind] == :function_no_proto

      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_result_type(function_proto)
      parameters = []
      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, true, false, 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
    elsif 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) click to toggle source
# File lib/ffi_gen.rb, line 775
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) click to toggle source
# File lib/ffi_gen.rb, line 710
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], Clang.get_type_spelling(full_type).to_s_and_dispose
  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
        type = @declarations_by_type[Clang.get_cursor_type(Clang.get_type_declaration(pointee_type))]
        type &&= ByReferenceType.new(type)
      when :function_proto, :function_no_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
        pointee_type = resolve_type pointee_type
        type = PointerType.new pointee_type, 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, :function_no_proto
    UnknownType.new
  when :incomplete_array
    ArrayType.new resolve_type(Clang.get_array_element_type(canonical_type)), nil
  else
    raise NotImplementedError, "No translation for values of type #{canonical_type[:kind]}"
  end
end
translation_unit() click to toggle source
# File lib/ffi_gen.rb, line 305
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