Class: RubyHeaderParser::Parser

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby_header_parser/parser.rb,
sig/ruby_header_parser/parser.rbs

Overview

parse ruby.h using ctags

Constant Summary collapse

DEFAULT_HEADER_FILE =

Returns:

  • (String)
"#{RbConfig::CONFIG["rubyhdrdir"]}/ruby.h".freeze
DEFAULT_INCLUDE_PATHS =

Returns:

  • (Array[String])
[
  RbConfig::CONFIG["rubyarchhdrdir"],
  RbConfig::CONFIG["rubyhdrdir"],
].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(dist_preprocessed_header_file: nil, header_file: DEFAULT_HEADER_FILE, include_paths: DEFAULT_INCLUDE_PATHS, config_file: nil) ⇒ Parser

Note:

dist_preprocessed_header_file is used as the output destination for temporary files when the parser is executed

Note:

See CONFIG.md for config file details

Returns a new instance of Parser.

Parameters:

  • header_file (String) (defaults to: DEFAULT_HEADER_FILE)

    Path to ruby.h

  • include_paths (Array<String>) (defaults to: DEFAULT_INCLUDE_PATHS)
  • dist_preprocessed_header_file (String, nil) (defaults to: nil)

    Destination path to the output of preprocessed ruby.h. (default: "#{Dir.tmpdir}/ruby_preprocessed.h")

  • config_file (String, nil) (defaults to: nil)

    Path to config file (default: config/default.yml.erb)



39
40
41
42
43
44
45
46
47
# File 'lib/ruby_header_parser/parser.rb', line 39

def initialize(dist_preprocessed_header_file: nil, header_file: DEFAULT_HEADER_FILE,
               include_paths: DEFAULT_INCLUDE_PATHS, config_file: nil)
  @header_file = header_file
  @include_paths = include_paths
  @dist_preprocessed_header_file = dist_preprocessed_header_file || File.join(Dir.tmpdir, "ruby_preprocessed.h")

  config_file ||= File.expand_path("../../config/default.yml.erb", __dir__.to_s)
  @config = Config.new(config_file)
end

Instance Attribute Details

#configRubyHeaderParser::Config (readonly)



20
21
22
# File 'lib/ruby_header_parser/parser.rb', line 20

def config
  @config
end

#dist_preprocessed_header_fileString (readonly)

Returns:

  • (String)


16
17
18
# File 'lib/ruby_header_parser/parser.rb', line 16

def dist_preprocessed_header_file
  @dist_preprocessed_header_file
end

#header_fileString (readonly)

Returns:

  • (String)


8
9
10
# File 'lib/ruby_header_parser/parser.rb', line 8

def header_file
  @header_file
end

#include_pathsArray<String> (readonly)

Returns:

  • (Array<String>)


12
13
14
# File 'lib/ruby_header_parser/parser.rb', line 12

def include_paths
  @include_paths
end

Instance Method Details

#__extract_function_definitions(c_kinds:, kind:, is_parse_multiline_definition:) ⇒ Array<RubyHeaderParser::FunctionDefinition>

Parameters:

  • c_kinds (String)
  • kind (String)
  • is_parse_multiline_definition (Boolean)
  • c_kinds: (String)
  • kind: (String)
  • is_parse_multiline_definition: (Boolean)

Returns:



123
124
125
126
127
128
129
# File 'lib/ruby_header_parser/parser.rb', line 123

def __extract_function_definitions(c_kinds:, kind:, is_parse_multiline_definition:)
  stdout = execute_ctags("--c-kinds=#{c_kinds} --fields=+nS --extras=+q")

  stdout.each_line.map do |line|
    generate_function_definition_from_line(line:, kind:, is_parse_multiline_definition:)
  end.compact.uniq(&:name)
end

#analyze_argument_type(function_name:, arg_pos:, parts:) ⇒ Array<String, Symbol, Integer>

Returns - type [String]

  • pointer [Symbol]
  • length [Integer].

Parameters:

  • function_name (String)
  • arg_pos (Integer)
  • parts (Array<String>)

Returns:

  • (Array<String, Symbol, Integer>)
    • type [String]
    • pointer [Symbol]
    • length [Integer]


322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
# File 'lib/ruby_header_parser/parser.rb', line 322

def analyze_argument_type(function_name:, arg_pos:, parts:)
  pointer, length = prepare_argument_parts(arg_pos:, parts:)
  type = parts[0...-1] || []
  original_type = Util.sanitize_type(type.join(" "))

  case original_type
  when /\*+$/
    type = original_type.gsub(/\*+$/, "").strip
    pointer = config.function_arg_pointer_hint(function_name:, pos: arg_pos)

  when /^void\s*/, /\(.*\)/
    # function pointer (e.g. void *(*func)(void *)) is treated as `void*`
    type = "void"
    pointer = config.function_arg_pointer_hint(function_name:, pos: arg_pos)

  else
    type = original_type
  end

  length = pointer_length(original_type) if pointer == :sref

  [type, pointer, length]
end

#create_typeref(definition:, function_name:, typeref_field:, filepath:, line_num:) ⇒ RubyHeaderParser::TyperefDefinition

Parameters:

  • definition (String)
  • function_name (String)
  • typeref_field (String, nil)
  • filepath (String)
  • line_num (Integer)

Returns:



233
234
235
236
237
238
239
240
241
242
243
# File 'lib/ruby_header_parser/parser.rb', line 233

def create_typeref(definition:, function_name:, typeref_field:, filepath:, line_num:)
  typeref_type = parse_typeref_type(definition:, function_name:, typeref_field:, filepath:, line_num:)

  typeref_pointer = nil
  if typeref_type.match?(/\*+$/)
    typeref_type = typeref_type.gsub(/\*+$/, "").strip
    typeref_pointer = config.function_self_pointer_hint(function_name)
  end

  TyperefDefinition.new(type: typeref_type, pointer: typeref_pointer)
end

#execute_ctags(args = "") ⇒ String

Parameters:

  • args (String) (defaults to: "")

Returns:

  • (String)


166
167
168
169
170
171
172
173
# File 'lib/ruby_header_parser/parser.rb', line 166

def execute_ctags(args = "")
  unless File.exist?(dist_preprocessed_header_file)
    include_args = include_paths.map { |path| "-I #{path}" }.join(" ")
    system("gcc -E #{include_args} #{header_file} -o #{dist_preprocessed_header_file}", exception: true)
  end

  `ctags --languages=C --language-force=C #{args} -f - #{dist_preprocessed_header_file}`
end

#extract_enum_definitionsArray<RubyHeaderParser::EnumDefinition>



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/ruby_header_parser/parser.rb', line 93

def extract_enum_definitions
  stdout = execute_ctags("--c-kinds=e --fields=+n")

  # Workaround for Ruby::UnannotatedEmptyCollection on steep 1.9.0+
  name_to_definition = {} #: Hash[String, RubyHeaderParser::EnumDefinition]

  name_to_definitions =
    stdout.each_line.with_object(name_to_definition) do |line, hash|
      parts = line.split("\t")

      enum_name = Util.find_field(parts, "enum")
      next unless enum_name

      value = parts[0]

      next unless config.should_generate_enum?(enum_name)

      hash[enum_name] ||= EnumDefinition.new(name: enum_name)
      hash[enum_name].values << value
    end

  name_to_definitions.values
end

#extract_function_definitionsArray<RubyHeaderParser::FunctionDefinition>



50
51
52
# File 'lib/ruby_header_parser/parser.rb', line 50

def extract_function_definitions
  __extract_function_definitions(c_kinds: "p", kind: "p", is_parse_multiline_definition: true)
end

#extract_static_inline_function_definitionsArray<RubyHeaderParser::FunctionDefinition>



55
56
57
# File 'lib/ruby_header_parser/parser.rb', line 55

def extract_static_inline_function_definitions
  __extract_function_definitions(c_kinds: "+p-d", kind: "f", is_parse_multiline_definition: false)
end

#extract_struct_definitionsArray<RubyHeaderParser::StructDefinition>



60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/ruby_header_parser/parser.rb', line 60

def extract_struct_definitions
  stdout = execute_ctags("--c-kinds=s --fields=+n")

  stdout.each_line.with_object([]) do |line, definitions|
    parts = line.split("\t")

    struct_name = parts[0]
    next unless config.should_generate_struct?(struct_name)

    definitions << StructDefinition.new(
      name: struct_name,
    )
  end
end

#extract_type_definitionsArray<RubyHeaderParser::TyperefDefinition>



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/ruby_header_parser/parser.rb', line 76

def extract_type_definitions
  stdout = execute_ctags("--c-kinds=t --fields=+n")

  stdout.each_line.with_object([]) do |line, definitions|
    parts = line.split("\t")

    type_name = parts[0]

    next unless config.should_generate_type?(type_name)

    definitions << TypeDefinition.new(
      name: type_name,
    )
  end.uniq(&:name)
end

#generate_argument_definition(function_name:, arg:, arg_pos:) ⇒ ArgumentDefinition

Parameters:

  • function_name (String)
  • arg (String)
  • arg_pos (Integer)

Returns:



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/ruby_header_parser/parser.rb', line 285

def generate_argument_definition(function_name:, arg:, arg_pos:)
  parts = arg.split

  if parts.count < 2
    return ArgumentDefinition.new(
      type:    parts[0],
      name:    "arg#{arg_pos}",
      pointer: nil,
    )
  end

  loop do
    pointer_index = parts.index("*")
    break unless pointer_index

    parts[pointer_index - 1] << "*"
    parts.delete_at(pointer_index)
  end

  type, pointer, length = analyze_argument_type(function_name:, arg_pos:, parts:)

  ArgumentDefinition.new(
    name:    parts[-1],
    type:,
    pointer:,
    length:,
  )
end

#generate_function_definition_from_line(line:, kind:, is_parse_multiline_definition:) ⇒ RubyHeaderParser::FunctionDefinition?

Parameters:

  • line (String)
  • kind (String)
  • is_parse_multiline_definition (Boolean)
  • line: (String)
  • kind: (String)
  • is_parse_multiline_definition: (Boolean)

Returns:



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/ruby_header_parser/parser.rb', line 136

def generate_function_definition_from_line(line:, kind:, is_parse_multiline_definition:)
  parts = line.split("\t")

  function_name = parts[0]
  filepath = parts[1]

  return nil unless config.should_generate_function?(function_name)

  return nil unless parts[3] == kind

  line_num = Util.find_field(parts, "line").to_i
  definition = parse_function_definition(filepath:, pattern: parts[2], line_num:, is_parse_multiline_definition:)

  args = parse_definition_args(function_name, Util.find_field(parts, "signature"))

  # Exclude functions with variable-length arguments
  return nil if args&.last&.type == "..."

  typeref_field = Util.find_field(parts, "typeref:typename")

  FunctionDefinition.new(
    definition:,
    name:       function_name,
    typeref:    create_typeref(definition:, function_name:, typeref_field:, filepath:, line_num:),
    args:,
  )
end

#parse_definition_args(function_name, signature) ⇒ Array<RubyHeaderParser::ArgumentDefinition>

Parameters:

  • function_name (String)
  • signature (String, nil)

Returns:



212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/ruby_header_parser/parser.rb', line 212

def parse_definition_args(function_name, signature)
  return [] unless signature

  signature = signature.strip.delete_prefix("(").delete_suffix(")")
  return [] if signature.match?(/^void$/i)

  args = Util.split_signature(signature)

  arg_pos = 0
  args.map do |arg|
    arg_pos += 1
    generate_argument_definition(function_name:, arg:, arg_pos:)
  end
end

#parse_function_definition(filepath:, pattern:, line_num:, is_parse_multiline_definition:) ⇒ String

Parameters:

  • filepath (String)
  • pattern (String)
  • line_num (Integer)
  • is_parse_multiline_definition (Boolean)

Returns:

  • (String)


196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/ruby_header_parser/parser.rb', line 196

def parse_function_definition(filepath:, pattern:, line_num:, is_parse_multiline_definition:)
  definition =
    if pattern.end_with?("$/;\"")
      pattern.delete_prefix("/^").delete_suffix("$/;\"")
    elsif is_parse_multiline_definition
      read_definition_from_header_file(filepath, line_num)
    else
      pattern.delete_prefix("/^")
    end

  definition.strip.delete_suffix(";")
end

#parse_typeref_type(definition:, function_name:, typeref_field:, filepath:, line_num:) ⇒ String

Parameters:

  • definition (String)
  • function_name (String)
  • typeref_field (String, nil)
  • filepath (String)
  • line_num (Integer)

Returns:

  • (String)


251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/ruby_header_parser/parser.rb', line 251

def parse_typeref_type(definition:, function_name:, typeref_field:, filepath:, line_num:)
  typeref_type =
    if typeref_field
      typeref_field.gsub(/[A-Z_]+\s*\(\(.*\)\)/, "")
    else
      # parse typeref in definition
      type = definition[0...definition.index(function_name)] || ""
      type.gsub("char *", "char*").strip
    end

  typeref_type = Util.sanitize_type(typeref_type)
  return typeref_type unless typeref_type.empty?

  # Check prev line
  line = read_file_line(filepath:, line_num: line_num - 1)
  return Util.sanitize_type(line) if line

  ""
end

#pointer_length(type) ⇒ Integer

Parameters:

  • type (String)
  • (String)

Returns:

  • (Integer)


374
375
376
377
# File 'lib/ruby_header_parser/parser.rb', line 374

def pointer_length(type)
  type =~ /(\*+)$/
  ::Regexp.last_match(1)&.length || 0
end

#prepare_argument_parts(parts:, arg_pos:) ⇒ Array<Symbol, Integer>

Returns - pointer [Symbol,nil]

  • length [Integer].

Parameters:

  • arg_pos (Integer)
  • parts (Array<String>)

Returns:

  • (Array<Symbol, Integer>)
    • pointer [Symbol,nil]
    • length [Integer]


352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/ruby_header_parser/parser.rb', line 352

def prepare_argument_parts(parts:, arg_pos:)
  if parts[-1] =~ /\[([0-9]+)?\]$/
    parts[-1].gsub!(/\[([0-9]+)?\]$/, "")
    length = ::Regexp.last_match(1).to_i

    unless parts[-1] =~ /^[0-9a-zA-Z_]+$/
      # last elements isn't dummy argument
      parts << "arg#{arg_pos}"
    end

    return [:array, length]
  end

  unless parts[-1] =~ /^[0-9a-zA-Z_]+$/
    # last elements isn't dummy argument
    parts << "arg#{arg_pos}"
  end

  [nil, 0]
end

#read_definition_from_header_file(file, line_num) ⇒ String

Parameters:

  • file (String)
  • line_num (Integer)

Returns:

  • (String)


177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/ruby_header_parser/parser.rb', line 177

def read_definition_from_header_file(file, line_num)
  definition = +""

  File.open(file, "r") do |f|
    f.each_with_index do |line, index|
      if index + 1 >= line_num
        definition << line.strip
        return definition if definition.end_with?(");")
      end
    end
  end
  ""
end

#read_file_line(filepath:, line_num:) ⇒ String?

Parameters:

  • filepath (String)
  • line_num (Integer)
  • filepath: (String)
  • line_num: (Integer)

Returns:

  • (String, nil)


273
274
275
276
277
278
# File 'lib/ruby_header_parser/parser.rb', line 273

def read_file_line(filepath:, line_num:)
  return nil if line_num < 1

  lines = File.open(filepath, "rb") { |f| f.readlines(chomp: true) }
  lines[line_num - 1]
end