Class: FFIGen

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

Defined Under Namespace

Classes: Constant, Enum, Function, Struct, Writer

Constant Summary collapse

RUBY_KEYWORDS =
%w{alias allocate and begin break case class def defined do else elsif end ensure false for if in initialize module next nil not or redo rescue retry return self super then true undef unless until when while yield}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ FFIGen

Returns a new instance of FFIGen.



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

def initialize(options = {})
  @ruby_module = options[:ruby_module] 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, []
  @blacklist   = options.fetch :blacklist, []
  @output      = options.fetch :output, $stdout
  
  blacklist = @blacklist
  @blacklist = lambda { |name| blacklist.include? name } if @blacklist.is_a? Array
  
  @translation_unit = nil
  @declarations = nil
end

Instance Attribute Details

#blacklistObject (readonly)

Returns the value of attribute blacklist.



282
283
284
# File 'lib/ffi_gen.rb', line 282

def blacklist
  @blacklist
end

#cflagsObject (readonly)

Returns the value of attribute cflags.



282
283
284
# File 'lib/ffi_gen.rb', line 282

def cflags
  @cflags
end

#ffi_libObject (readonly)

Returns the value of attribute ffi_lib.



282
283
284
# File 'lib/ffi_gen.rb', line 282

def ffi_lib
  @ffi_lib
end

#headersObject (readonly)

Returns the value of attribute headers.



282
283
284
# File 'lib/ffi_gen.rb', line 282

def headers
  @headers
end

#outputObject (readonly)

Returns the value of attribute output.



282
283
284
# File 'lib/ffi_gen.rb', line 282

def output
  @output
end

#ruby_moduleObject (readonly)

Returns the value of attribute ruby_module.



282
283
284
# File 'lib/ffi_gen.rb', line 282

def ruby_module
  @ruby_module
end

Class Method Details

.generate(options = {}) ⇒ Object



589
590
591
# File 'lib/ffi_gen.rb', line 589

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

Instance Method Details

#declarationsObject



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

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| 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_ptr = FFI::MemoryPointer.new :pointer
    Clang.get_spelling_location Clang.get_cursor_location(declaration), file_ptr, nil, nil, nil
    file = file_ptr.read_pointer
    
    extent = Clang.get_cursor_extent declaration
    comment_range = Clang.get_range previous_declaration_end, Clang.get_range_start(extent)
    unless declaration[:kind] == :enum_decl or declaration[:kind] == :struct_decl # keep comment for typedef_decl
      previous_declaration_end = Clang.get_range_end extent
    end 
    
    next if not header_files.include? file
    
    name = Clang.get_cursor_spelling(declaration).to_s_and_dispose
    next if @blacklist[name]
    
    comment = extract_comment translation_unit, comment_range
    
    case declaration[:kind]
    when :enum_decl, :struct_decl
      read_named_declaration declaration, name, comment unless name.empty?
    
    when :function_decl
      function = Function.new self, name, false, comment
      function.return_type = Clang.get_cursor_result_type declaration
      @declarations[name] = function
      
      Clang.get_children(declaration).each do |function_child|
        next if function_child[:kind] != :parm_decl
        param_name = Clang.get_cursor_spelling(function_child).to_s_and_dispose
        param_type = Clang.get_cursor_type function_child
        function.parameters << { name: param_name, type: param_type}
      end
    
    when :typedef_decl
      typedef_children = Clang.get_children declaration
      if typedef_children.size == 1
        read_named_declaration typedef_children.first, name, comment unless @declarations.has_key? name
        
      elsif typedef_children.size > 1
        callback = Function.new self, name, true, comment
        callback.return_type = Clang.get_cursor_type typedef_children.first
        @declarations[name] = callback
        
        typedef_children[1..-1].each do |param_decl|
          param_name = 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_ptr_ptr = FFI::MemoryPointer.new :pointer
      num_tokens_ptr = FFI::MemoryPointer.new :uint
      Clang.tokenize translation_unit, extent, tokens_ptr_ptr, num_tokens_ptr
      num_tokens = num_tokens_ptr.read_uint
      tokens_ptr = FFI::Pointer.new Clang::Token, tokens_ptr_ptr.read_pointer
      
      if num_tokens == 3
        token = Clang::Token.new tokens_ptr[1]
        if Clang.get_token_kind(token) == :literal
          value = Clang.get_token_spelling(translation_unit, token).to_s_and_dispose
          @declarations[name] = Constant.new self, name, value
        end 
      end
          
    end
  end

  @declarations
end

#extract_comment(translation_unit, range) ⇒ Object



477
478
479
480
481
482
483
484
485
486
487
488
# File 'lib/ffi_gen.rb', line 477

def extract_comment(translation_unit, range)
  tokens_ptr_ptr = FFI::MemoryPointer.new :pointer
  num_tokens_ptr = FFI::MemoryPointer.new :uint
  Clang.tokenize translation_unit, range, tokens_ptr_ptr, num_tokens_ptr
  num_tokens = num_tokens_ptr.read_uint
  tokens_ptr = FFI::Pointer.new Clang::Token, tokens_ptr_ptr.read_pointer
  (num_tokens - 1).downto(0) do |i|
    token = Clang::Token.new tokens_ptr[i]
    return Clang.get_token_spelling(translation_unit, token).to_s_and_dispose if Clang.get_token_kind(token) == :comment
  end
  ""
end

#generateObject



407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# File 'lib/ffi_gen.rb', line 407

def generate
  writer = Writer.new
  writer.puts "# Generated by ffi_gen. Please do not change this file by hand.", "", "require 'ffi'", "", "module #{@ruby_module}"
  writer.indent do
    writer.puts "extend FFI::Library", "ffi_lib '#{@ffi_lib}'", ""
    declarations.each do |name, declaration|
      declaration.write writer
    end
  end
  writer.puts "end"
  if @output.is_a? String
    File.open(@output, "w") { |file| file.write writer.output }
    puts "ffi_gen: #{@output}"
  else
    @output.write writer.output
  end
end

#read_named_declaration(declaration, name, comment) ⇒ Object



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

def read_named_declaration(declaration, name, comment)
  case declaration[:kind]
  when :enum_decl
    enum = Enum.new self, name, comment
    @declarations[name] = enum
    
    previous_constant_location = Clang.get_cursor_location declaration
    Clang.get_children(declaration).each do |enum_constant|
      constant_name = Clang.get_cursor_spelling(enum_constant).to_s_and_dispose
      
      constant_value = nil
      value_cursor = Clang.get_children(enum_constant).first
      constant_value = value_cursor && case value_cursor[:kind]
      when :integer_literal
        tokens_ptr_ptr = FFI::MemoryPointer.new :pointer
        num_tokens_ptr = FFI::MemoryPointer.new :uint
        Clang.tokenize translation_unit, Clang.get_cursor_extent(value_cursor), tokens_ptr_ptr, num_tokens_ptr
        token = Clang::Token.new tokens_ptr_ptr.read_pointer
        literal = Clang.get_token_spelling translation_unit, token
        Clang.dispose_tokens translation_unit, tokens_ptr_ptr.read_pointer, num_tokens_ptr.read_uint
        literal
      else
        next # skip those entries for now
      end
      
      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
      
      enum.constants << { name: constant_name, value: constant_value, comment: constant_comment }
    end
    
  when :struct_decl
    struct = Struct.new self, name, comment
    @declarations[name] = struct
    
    previous_field_location = Clang.get_cursor_location declaration
    Clang.get_children(declaration).each do |field_decl|
      field_name = Clang.get_cursor_spelling(field_decl).to_s_and_dispose
      field_type = Clang.get_cursor_type field_decl
      
      field_location = Clang.get_cursor_location field_decl
      field_comment_range = Clang.get_range previous_field_location, field_location
      field_comment = extract_comment translation_unit, field_comment_range
      previous_field_location = field_location
      
      struct.fields << { name: field_name, type: field_type, comment: field_comment }
    end
  end
end

#to_ffi_type(full_type) ⇒ Object



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

def to_ffi_type(full_type)
  declaration = Clang.get_type_declaration full_type
  name = Clang.get_cursor_spelling(declaration).to_s_and_dispose
  return @declarations[name].reference if @declarations.has_key? name
  
  canonical_type = Clang.get_canonical_type full_type
  case canonical_type[:kind]
  when :void then ":void"
  when :bool then ":bool"
  when :u_char then ":uchar"
  when :u_short then ":ushort"
  when :u_int then ":uint"
  when :u_long then ":ulong"
  when :u_long_long then ":ulong_long"
  when :char_s, :s_char then ":char"
  when :short then ":short"
  when :int then ":int"
  when :long then ":long"
  when :long_long then ":long_long"
  when :float then ":float"
  when :double then ":double"
  when :pointer
    pointee_type = Clang.get_pointee_type canonical_type
    pointee_type[:kind] == :char_s ? ":string" : ":pointer"
  when :constant_array
    element_type = Clang.get_array_element_type canonical_type
    size = Clang.get_array_size canonical_type
    "[#{to_ffi_type element_type}, #{size}]"
  else
    raise NotImplementedError, "No translation for values of type #{canonical_type[:kind]}"
  end
end

#to_ruby_camelcase(str) ⇒ Object



581
582
583
584
585
586
587
# File 'lib/ffi_gen.rb', line 581

def to_ruby_camelcase(str)
  str = str.dup
  str.sub! /^(#{@prefixes.join('|')})/, '' # remove prefixes
  str.gsub!(/(^|_)[a-z]/) { |match| match.upcase } # make word beginnings upcased
  str.gsub! '_', '' # remove all underscores
  str
end

#to_ruby_lowercase(str, avoid_keywords = false) ⇒ Object



568
569
570
571
572
573
574
575
576
577
578
579
# File 'lib/ffi_gen.rb', line 568

def to_ruby_lowercase(str, avoid_keywords = false)
  str = str.dup
  str.sub! /^(#{@prefixes.join('|')})/, '' # remove prefixes
  str.gsub! /([A-Z][a-z])/, '_\1' # add underscores before word beginnings
  str.gsub! /([a-z])([A-Z])/, '\1_\2' # add underscores after word endings
  str.sub! /^_*/, '' # remove underscores at the beginning
  str.gsub! /__+/, '_' # replace multiple underscores by only one
  str.downcase!
  str.sub! /^\d/, '_\0' # fix illegal beginnings
  str = "_#{str}" if avoid_keywords and RUBY_KEYWORDS.include? str
  str
end

#to_type_name(full_type, short = false) ⇒ Object



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

def to_type_name(full_type, short = false)
  declaration = Clang.get_type_declaration full_type
  name = Clang.get_cursor_spelling(declaration).to_s_and_dispose
  return @declarations[name].type_name(short) if @declarations.has_key? name
  
  canonical_type = Clang.get_canonical_type full_type
  case canonical_type[:kind]
  when :void then "nil"
  when :bool then "Boolean"
  when :u_char, :u_short, :u_int, :u_long, :u_long_long, :char_s, :s_char, :short, :int, :long, :long_long then "Integer"
  when :float, :double then "Float"
  when :pointer
    pointee_type = Clang.get_pointee_type canonical_type
    if pointee_type[:kind] == :char_s
      "String"
    else
      pointer_depth = 0
      pointer_target_name = ""
      current_type = full_type
      loop do
        declaration = Clang.get_type_declaration current_type
        pointer_target_name = to_ruby_camelcase 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 = Clang.get_type_kind_spelling(current_type[:kind]).to_s_and_dispose
          break
        end
      end
      short ? pointer_target_name : "FFI::Pointer(#{'*' * pointer_depth}#{pointer_target_name})"
    end
  when :constant_array
    element_type = Clang.get_array_element_type canonical_type
    "Array<#{to_type_name element_type}>"
  else
    raise NotImplementedError, "No type name for type #{canonical_type[:kind]}"
  end
end

#translation_unitObject



300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/ffi_gen.rb', line 300

def translation_unit
  return @translation_unit unless @translation_unit.nil?
  
  args = []
  @headers.each do |header|
    args.push "-include", header
  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