Class: FFIDB::HeaderParser

Inherits:
Object
  • Object
show all
Defined in:
lib/ffidb/header_parser.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(base_directory: nil, debug: nil) ⇒ HeaderParser

Returns a new instance of HeaderParser.

Parameters:

  • base_directory (Pathname, #to_s) (defaults to: nil)


23
24
25
26
27
28
29
30
31
32
33
# File 'lib/ffidb/header_parser.rb', line 23

def initialize(base_directory: nil, debug: nil)
  require 'ffi/clang' # https://rubygems.org/gems/ffi-clang

  @base_directory = base_directory
  @debug = debug
  @defines = {}
  @include_paths = []
  @include_symbols = {}
  @exclude_symbols = {}
  @clang_index = FFI::Clang::Index.new
end

Instance Attribute Details

#base_directoryObject (readonly)

Returns the value of attribute base_directory.



14
15
16
# File 'lib/ffidb/header_parser.rb', line 14

def base_directory
  @base_directory
end

#debugObject (readonly)

Returns the value of attribute debug.



15
16
17
# File 'lib/ffidb/header_parser.rb', line 15

def debug
  @debug
end

#definesObject (readonly)

Returns the value of attribute defines.



16
17
18
# File 'lib/ffidb/header_parser.rb', line 16

def defines
  @defines
end

#exclude_symbolsObject (readonly)

Returns the value of attribute exclude_symbols.



19
20
21
# File 'lib/ffidb/header_parser.rb', line 19

def exclude_symbols
  @exclude_symbols
end

#include_pathsObject (readonly)

Returns the value of attribute include_paths.



17
18
19
# File 'lib/ffidb/header_parser.rb', line 17

def include_paths
  @include_paths
end

#include_symbolsObject (readonly)

Returns the value of attribute include_symbols.



18
19
20
# File 'lib/ffidb/header_parser.rb', line 18

def include_symbols
  @include_symbols
end

Instance Method Details

#add_include_path!(path) ⇒ void

This method returns an undefined value.

Parameters:

  • path (Pathname, #to_s)


55
56
57
# File 'lib/ffidb/header_parser.rb', line 55

def add_include_path!(path)
  self.include_paths << Pathname(path)
end

#define_macro!(var, val = 1) ⇒ void

This method returns an undefined value.

Parameters:

  • var (Symbol, #to_sym)
  • val (String, #to_s) (defaults to: 1)


48
49
50
# File 'lib/ffidb/header_parser.rb', line 48

def define_macro!(var, val = 1)
  self.defines[var.to_sym] = val.to_s
end

#parse_enum(declaration, typedef_name: nil) ⇒ Enum

Parameters:

  • declaration (FFI::Clang::Cursor)
  • typedef_name (String) (defaults to: nil)

Returns:



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/ffidb/header_parser.rb', line 169

def parse_enum(declaration, typedef_name: nil)
  enum_name = declaration.spelling
  enum_name = typedef_name if enum_name.empty?
  FFIDB::Enum.new(enum_name).tap do |enum|
    declaration.visit_children do |node, _|
      case node.kind
        when :cursor_enum_constant_decl
          k = node.spelling
          v = node.enum_value
          enum.values[k] = v
      end
      :continue # visit the next sibling
    end
  end
end

#parse_function(declaration) ⇒ Function

Parameters:

  • declaration (FFI::Clang::Cursor)

Returns:



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/ffidb/header_parser.rb', line 224

def parse_function(declaration)
  name = declaration.spelling
  comment = declaration.comment&.text
  function = FFIDB::Function.new(
    name: name,
    type: self.parse_type(declaration.type.result_type),
    parameters: {},
    definition: nil, # set in #parse_header()
    comment: comment && !(comment.empty?) ? comment : nil,
  )
  declaration.visit_children do |node, _|
    case node.kind
      when :cursor_parm_decl
        default_name = "_#{function.parameters.size + 1}"
        parameter = self.parse_parameter(node, default_name: default_name)
        function.parameters[parameter.name.to_sym] = parameter
    end
    :continue # visit the next sibling
  end
  function.parameters.freeze
  function.instance_variable_set(:@debug, declaration.type.spelling.sub(/\s*\(/, " #{name}(")) if self.debug # TODO: __attribute__((noreturn))
  function
end

#parse_header(path) {|exception| ... } ⇒ Header

Parameters:

  • path (Pathname, #to_s)

Yields:

  • (exception)

Returns:

Raises:

  • (ParsePanic)

    if parsing encounters a fatal error



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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/ffidb/header_parser.rb', line 64

def parse_header(path)
  path = Pathname(path.to_s) unless path.is_a?(Pathname)
  name = (self.base_directory ? path.relative_path_from(self.base_directory) : path).to_s
  args = self.defines.inject([]) { |r, (k, v)| r << "-D#{k}=#{v}" }
  args += self.include_paths.map { |p| "-I#{p}" }

  translation_unit = nil
  begin
    translation_unit = @clang_index.parse_translation_unit(path.to_s, args)
  rescue FFI::Clang::Error => error
    raise ParsePanic.new(error.to_s)
  end

  translation_unit.diagnostics.each do |diagnostic|
    exception_class = case diagnostic.severity.to_sym
      when :fatal then raise ParsePanic.new(diagnostic.format)
      when :error then ParseError
      when :warning then ParseWarning
      else ParseWarning
    end
    yield exception_class.new(diagnostic.format)
  end

  okayed_files = {}
  FFIDB::Header.new(name: name, typedefs: [], enums: [], structs: [], unions: [], functions: []).tap do |header|
    root_cursor = translation_unit.cursor
    root_cursor.visit_children do |declaration, _|
      location = declaration.location
      location_file = location.file
      if (okayed_files[location_file] ||= self.consider_path?(location_file))
        case declaration.kind
          when :cursor_typedef_decl
            typedef = self.parse_typedef(declaration) do |symbol|
              case
                when symbol.enum? then header.enums << symbol
                when symbol.struct? then header.structs << symbol
                when symbol.union? then header.unions << symbol
              end
            end
            header.typedefs << typedef if typedef
          when :cursor_enum_decl
            enum_name = declaration.spelling
            if enum_name && !enum_name.empty?
              header.enums << self.parse_enum(declaration)
            end
          when :cursor_struct
            struct_name = declaration.spelling
            if struct_name && !struct_name.empty?
              if (struct = self.parse_struct(declaration))
                header.structs << struct
              end
            end
          when :cursor_union
            union_name = declaration.spelling
            if union_name && !union_name.empty?
              if (union = self.parse_union(declaration))
                header.unions << union
              end
            end
          when :cursor_function
            function_name = declaration.spelling
            if self.consider_function?(function_name)
              function = self.parse_function(declaration)
              function.definition = self.parse_location(location)
              header.functions << function
            end
          else # TODO: other declarations of interest?
        end
      end
      :continue # visit the next sibling
    end
    header.comment = root_cursor.comment&.text
  end
end

#parse_location(location) ⇒ Location

Parameters:

  • location (FFI::Clang::ExpansionLocation)

Returns:



301
302
303
304
305
306
307
# File 'lib/ffidb/header_parser.rb', line 301

def parse_location(location)
  return nil if location.nil?
  FFIDB::Location.new(
    file: location.file ? self.make_relative_path(location.file).to_s : nil,
    line: location.line,
  )
end

#parse_macro!(var_and_val) ⇒ void

This method returns an undefined value.

Parameters:

  • var_and_val (String, #to_s)


38
39
40
41
42
# File 'lib/ffidb/header_parser.rb', line 38

def parse_macro!(var_and_val)
  var, val = var_and_val.to_s.split('=', 2)
  val = 1 if val.nil?
  self.define_macro! var, val
end

#parse_parameter(declaration, default_name: '_') ⇒ Parameter

Parameters:

  • declaration (FFI::Clang::Cursor)
  • default_name (String, #to_s) (defaults to: '_')

Returns:



252
253
254
255
256
257
# File 'lib/ffidb/header_parser.rb', line 252

def parse_parameter(declaration, default_name: '_')
  name = declaration.spelling
  type = self.parse_type(declaration.type)
  FFIDB::Parameter.new(
    ((name.nil? || name.empty?) ? default_name.to_s : name).to_sym, type)
end

#parse_struct(declaration, typedef_name: nil) ⇒ Struct

Parameters:

  • declaration (FFI::Clang::Cursor)
  • typedef_name (String) (defaults to: nil)

Returns:



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/ffidb/header_parser.rb', line 189

def parse_struct(declaration, typedef_name: nil)
  struct_name = declaration.spelling
  struct_name = typedef_name if struct_name.empty?
  FFIDB::Struct.new(struct_name).tap do |struct|
    declaration.visit_children do |node, _|
      case node.kind
        when :cursor_field_decl
          field_name = node.spelling
          field_type = nil
          node.visit_children do |node, _|
            case node.kind
              when :cursor_type_ref
                field_type = node.spelling
                :break
              else :continue
            end
          end
          struct.fields[field_name.to_sym] = Type.for(field_type)
      end
      :continue # visit the next sibling
    end
  end
end

#parse_type(type) ⇒ Type

Parameters:

  • type (FFI::Clang::Type)

Returns:



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/ffidb/header_parser.rb', line 262

def parse_type(type)
  ostensible_type = type.spelling
  ostensible_type.sub!(/\*const$/, '*') # remove private const qualifiers
  pointer_suffix = case ostensible_type
    when /(\s\*+)$/
      ostensible_type.delete_suffix!($1)
      $1
    else nil
  end
  resolved_type = if self.preserve_type?(ostensible_type)
    ostensible_type << pointer_suffix if pointer_suffix
    ostensible_type
  else
    type.canonical.spelling
  end
  resolved_type.sub!(/\*const$/, '*') # remove private const qualifiers
  Type.for(resolved_type)
end

#parse_typedef(declaration, &block) ⇒ Typedef

Parameters:

  • declaration (FFI::Clang::Cursor)

Returns:



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/ffidb/header_parser.rb', line 142

def parse_typedef(declaration, &block)
  typedef_name = declaration.spelling
  typedef_type = nil
  declaration.visit_children do |node, _|
    node_name = node.spelling
    case node.kind
      when :cursor_type_ref
        typedef_type = node_name
      when :cursor_enum_decl
        typedef_type = "enum #{node_name}".rstrip
        yield self.parse_enum(node, typedef_name: typedef_name)
      when :cursor_struct
        typedef_type = "struct #{node_name}".rstrip
        yield self.parse_struct(node, typedef_name: typedef_name)
      when :cursor_union
        typedef_type = "union #{node_name}".rstrip
        #yield self.parse_union(node, typedef_name: typedef_name) # TODO
    end
    :continue # visit the next sibling
  end
  FFIDB::Typedef.new(typedef_name, typedef_type) if typedef_type
end

#parse_union(declaration, typedef_name: nil) ⇒ Union

Parameters:

  • declaration (FFI::Clang::Cursor)
  • typedef_name (String) (defaults to: nil)

Returns:



217
218
219
# File 'lib/ffidb/header_parser.rb', line 217

def parse_union(declaration, typedef_name: nil)
  # TODO: parse union declarations
end

#preserve_type?(type_name) ⇒ Boolean

Parameters:

  • type_name (String, #to_s)

Returns:

  • (Boolean)


284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/ffidb/header_parser.rb', line 284

def preserve_type?(type_name)
  case type_name.to_s
    when 'va_list' then true                          # <stdarg.h>
    when '_Bool'  then true                           # <stdbool.h>
    when 'size_t', 'wchar_t' then true                # <stddef.h>
    when 'const size_t', 'const wchar_t' then true    # <stddef.h> # FIXME: need a better solution
    when /^u?int\d+_t$/, /^u?int\d+_t \*$/ then true  # <stdint.h>
    when /^u?intptr_t$/ then true                     # <stdint.h>
    when 'FILE' then true                             # <stdio.h>
    when 'ssize_t', 'off_t', 'off64_t' then true      # <sys/types.h>
    else false
  end
end