require 'rubygems'
require_gem 'ruby-breakpoint'
require_gem 'cast'
require 'open3'
# This is generally useful
#
class String
def pad(to_len, with = ' ')
pad = ''
self.length.upto(to_len) do pad << with end
return pad << self
end
end
class Autobindings
# Outputs a little prettier headings
#
def title(text, glyph = '-', size = 65)
'#' + ''.pad(size, glyph) + "\n" +
'# ' + text + "\n" +
'#' + ''.pad(size, glyph) + "\n"
end
# Setup our defaults
#
def initialize()
@gcc_cmd = '/usr/bin/gcc'
@gcc_params = '-E -P -w'
@gcc_extra_params = ''
@debug = false
@debug_lines = false
@graph = false
@headers_path = ''
@headers = []
@config = {}
@handmade = nil
end
# Various parameters. See documentation for exact meaning
#
attr_accessor :handmade, :gcc_cmd, :gcc_extra_params, :headers_path,
:debug, :debug_lines, :graph
attr_reader :config
def config=(cfg)
@config = cfg
@config[:external_types] = [] if @config[:external_types].nil?
@config[:type_aliases] = {} if @config[:type_aliases].nil?
@config[:gcc_params] = '' if @config[:gcc_params].nil?
end
# Feed the header files to gcc for preprocessing, read the output and strip linemarkers
#
def run_preprocessor
if @debug then
puts title( "Preprocessing header files" )
puts "Preprocessor: #{@gcc_cmd}"
puts "Headers directory: #{@headers_path}"
puts "Headers that will be processed: #{@config[:headers].join(', ')}"
puts "Extra options from commandline: #{@gcc_extra_params}"
puts "Extra options from config file: #{@config[:gcc_params]}"
end
prevcur = Dir.pwd
Dir.chdir(@headers_path) # move to the headers dir, so we can pass just to names to the preprocessor instead of long paths
params = [@gcc_params, @gcc_extra_params, @config[:gcc_params]].join(' ')
cmd = [@gcc_cmd, params, @config[:headers].join(' ')].join(' ')
puts "Resulting command line is below:\n-- #{cmd}" if @debug
@data = ''
gcc_err = ''
Open3.popen3(cmd) { |stdin, stdout, stderr|
stdout.read.each_line { |line|
next if line[0].chr == '#'
line = line.gsub(/__extension__/, '') # GCC extensions choke the C parser, this is the only way i have to remove them
@data << line
}
gcc_err = stderr.read
}
puts gcc_err if @debug
Dir.chdir(prevcur) # get back where we were, you never know...
if @debug_lines then
puts title("Preprocessor output:")
i = 1
part = ''
@data.each_line { |l| puts "[#{i.to_s.pad(3,'0')}] " << l; i+=1; }
end
end
# parse all the data and extract only the information that is meaningful to our generator
#
def parse_data
extra_types = @config[:external_types]
extra_types = [] if extra_types.nil?
if @debug then
puts title( "Parsing C headers" )
puts "#{extra_types.length} extra types will be passed to the C parser:\n#{extra_types.join(', ')}"
end
parser = C::Parser.new
parser.type_names.merge(extra_types)
#~ puts "--------------------------------"
#~ puts @data
#~ puts "--------------------------------"
tree = parser.parse(@data)
@enums = {}
@types = []
@decls = []
@structs = []
@protos = {}
@synon = {}
@all_synon = {}
tree.entities.each { |e|
if !(C::Declaration === e)
puts "Skipping #{e.class.to_s}" if @debug
next
end
if e.typedef?
# check if this a typedef defining a pointer to function, which we will use for callbacks
fp = FunctionPtr.parse(e)
if !fp.nil? then
@protos[fp.name] = fp.to_s(false, false)
else
name = e.type.to_s
name << e.declarators[0].indirect_type.to_s if C::Pointer === e.declarators[0].indirect_type
@all_synon[name] = e.declarators[0].name
end
end
# -- ENUMERATION
if C::Enum === e.type then
if e.type.name.nil?
puts "Skipping anonymous enum" if @debug
next
end
next if e.type.members.nil? # the enum was already defined, this is just the typedef. todo: refactor into the typedef case
@types << 'enum ' + e.type.name
lv = 0
en = (@enums[e.type.name] = {})
e.type.members.each { |mem|
if mem.val.nil? then
en[mem.name] = lv.to_i
lv += 1
else
# todo: Yes, this is very very brittle. There are a lot more arithmetics that
# someone can do inside an enum that would screw up this. However this is the most used stuff.
#
if C::BitOr === mem.val
en[mem.name.to_s] = en[mem.val.expr1.to_s].to_i | en[mem.val.expr2.to_s].to_i
elsif C::BitAnd === mem.val
en[mem.name.to_s] = en[mem.val.expr1.to_s].to_i & en[mem.val.expr2.to_s].to_i
elsif C::ShiftLeft === mem.val
en[mem.name.to_s] = en[mem.val.expr1.to_s].to_i << en[mem.val.expr2.to_s].to_i
elsif C::ShiftRight === mem.val
en[mem.name.to_s] = en[mem.val.expr1.to_s].to_i >> en[mem.val.expr2.to_s].to_i
elsif C::Negative === mem.val
en[mem.name.to_s] = en[mem.val.expr.to_s].to_i * -1
else
en[mem.name.to_s] = mem.val.val.to_i
end
end
}
# -- STRUCTURE (todo: not yet implemented)
elsif C::Struct === e.type then
@types << 'struct ' + e.type.name
@structs << [e.type.name, []]
@synon[e.type.to_s] = @all_synon[e.type.to_s]
# st = (@structs[e.type.name] = [])
# e.type.members.each { |mem|
# #todo: nested structures, initializers, etc
# st << [mem.declarators[0].name, mem.declarators[0].type.to_s]
#}
# -- FUNCTION DECLARATION
elsif e.declarators.length > 0 && C::Function === e.declarators[0].type
functype = e.declarators[0].indirect_type
decl = {}
decl[:name] = e.declarators[0].name
decl[:varargs] = e.declarators[0].type.var_args?
decl[:full] = e.type.to_s + FunctionPtr.pointers(functype.type)
decl[:full] << ' ' << decl[:name] << ' ('
functype.params.each_with_index { |p,i|
if (C::CustomType === p.type && p.type.name == 'va_list') then
decl[:varargs] = true
next # we leave this last va_args param out since it's a placeholder for the actual varargs params anyway
end
decl[:full] << ((i != 0) ? ', ' : '')
if C::Pointer === p.type &&
(C::Int === p.type.direct_type || C::Float === p.type.direct_type) then
decl[:full] << p.type.direct_type.to_s << ' ref'
else
fp = FunctionPtr.parse(p)
if fp.nil? then decl[:full] << p.type.to_s
else
# one of the function parameters is a pointer to an anonymous function
# we add it as void*, but then add also a prototype so it's possible to create
# a callback for it (use Ewl_Funcname_ParN as callback name)
#
decl[:full] << 'void*'
pname = decl[:name].split('_').map { |s| s.capitalize }.push('Arg'+i.to_s).join('_')
fp.name = pname
@protos[pname] = fp.to_s(false, false)
end
end
} unless functype.params.nil?
decl[:full] << ")"
#breakpoint if functype.params.length > 0 unless functype.params.nil?
@decls << decl
else
@synon[e.type.to_s] = @all_synon[e.type.to_s]
end
}
puts to_s if @debug
classgraph if @graph
end
class FunctionPtr
attr_accessor :name, :ret, :params
# Node shall be a C::Node
# Note: if the function ptr returns a pointer to another function, we don't handle that case.
# Either typedef it (saner) or make this routine more powerful.
#
def self.parse(node)
f = FunctionPtr.new
if C::Declaration === node then # the function pointer is in a typedef
if node.typedef? then
dec = node.declarators[0]
return nil unless !dec.nil? && C::Pointer === dec.indirect_type &&
C::Function === dec.indirect_type.type
f.name = dec.name
ftype = dec.indirect_type.type
f.ret = node.type.to_s # return type without any pointer qualifier
f.ret << FunctionPtr.pointers(ftype.type)
f.params = []
ftype.params.each { |par|
p = FunctionPtr.parse(par)
f.params << (( p.nil? ) ? par.type.to_s : p)
} unless ftype.params.nil?
return f
end
else
return nil unless node.type.is_a?(C::Type) &&
C::Pointer === node.type && C::Function === node.type.type
ftype = node.type.type
f.params = []
ftype.params.each { |par|
p = FunctionPtr.parse(par)
f.params << (( p.nil? ) ? par.type.to_s : p)
}
f.name = ftype.name if ftype.respond_to?(:name)
f.ret = ' '
while C::Pointer === ftype.type # type of function only stores pointer qualifiers in this case
f.ret << '*'
ftype = ftype.type
end
f.ret.insert(0, ftype.type.to_s)
return f
end
return nil
end
def to_s(anon=true, as_ptr=true)
s = @ret + ' '
s << '(*' if as_ptr
s << @name unless anon
s << ')' if as_ptr
s << " (#{@params.map { |par| par.to_s }.join(', ')})"
s
end
def self.pointers(ctype)
p = ''
while C::Pointer === ctype
p << '*'
ctype = ctype.type
end
return p
end
end
def ignored?(name)
@config[:ignore].each { |ig|
if (Regexp === ig && name.match(ig)) ||
(String == ig && name == ig)
puts "Ignoring symbol as requested: #{name}"
return true
end
}
return false
end
def create_dl_code
declarations = ''
@decls.each { |d|
next if ignored?(d[:name])
declarations << "\textern '#{d[:full]}'"
declarations << ", :varargs" if d[:varargs]
declarations << "\n"
}
aliases = @config[:external_types].collect { |typ|
talias = @config[:type_aliases][typ]
talias = 'void' if talias.nil?
"\ttypealias '#{typ}', '#{talias}'"
}
internal_aliases = ""
@synon.each { |name, typ|
internal_aliases << "\ttypealias '#{typ}', '#{name}'\n"
}
prototypes = ''
@protos.each { |name, decl|
prototypes << "\tprototype '#{decl}'\n" unless ignored?(name)
talias = @config[:type_aliases][name]
talias = 'void*' if talias.nil?
aliases << "\ttypealias '#{name}', '#{talias}'"
}
aliases = aliases.join("\n")
@config[:type_aliases].each { |typ, ali|
next unless @config[:external_types].index(typ).nil?
aliases << "\n\ttypealias '#{typ}', '#{ali}'"
}
enum_aliases = ''
enumpool = "\tclass Constants\n"
@enums.each { |enum, members|
enumpool << "\t\t#----- #{enum.to_s}\n"
enum_aliases << "\ttypealias '#{enum.to_s}', 'int'\n"
members.each { |member, value|
enumpool << "\t\t" << member.to_s.upcase << ' = ' << value.to_s << "\n"
}
enumpool << "\n"
}
enumpool << "\tend\n"
struct_aliases = ''
@structs.each { |struct| struct_aliases << "\ttypealias '#{struct}*', 'void*'\n" }
base_class = nil
classes = ""
@config[:classes].each { |kl|
classes << "\nclass #{kl[:name]} < "
# this is the "base class" of the module and get special treatment.
# there should be only one of these
if kl[:child_of].nil? then
if !base_class.nil?
puts "[ERROR] There should be only one \"base class\" for each module. You have #{base_class} and #{kl[:name]} here, one should die."
return exit
else
classes << "CClass"
classes << "\n\tLIB_MODULE = Lib#{@config[:module_base_name]}"
classes << "\n\textend CClass::CallbackMaker"
base_class = kl[:name]
end
else
classes << "#{kl[:child_of]}"
end
classes << "\n"
classes << "\twraps_class '#{kl[:wraps]}'\n" unless kl[:wraps].nil?
classes << "end\n"
}
if base_class.nil?
puts "[ERROR] There should be at least one \"base class\" for each module. It will be used to catch all calls that aren't handled by any object in the hierarchy."
return exit
end
handmade = ""
if @handmade
handmade << IO.read(@handmade)
end
libname = @config[:library_name]
libname = "lib#{@config[:module_base_name].downcase}" if libname.nil?
dl = <<-END_DL_CODE
#-------------------------------------------------------
# This piece of wrapper code was automatically generated
#-------------------------------------------------------
# Basic Ruby/DL module, which contains all the declarations of the wrapped functions, type aliases, callbacks
#
require File.dirname(File.expand_path(__FILE__)) + '/../dl_auto.rb'
module Lib#{@config[:module_base_name]}
extend DL::Importable
dlload '#{libname}.so'
BASE_NAME = '#{@config[:module_base_name].downcase}'
# Type aliases (from external types)
#
#{aliases}
# Type aliases (from enumerations)
#
#{enum_aliases}
# Type aliases (from structures)
#
#{struct_aliases}
# Type aliases (internal from the headers)
#
#{internal_aliases}
# Function declarations
#
#{declarations}
# Callback prototypes
#
#{prototypes}
# Constants (from enumerations)
#
#{enumpool}
end
module #{@config[:module_base_name]}
#------------------------------------------------------
# Class definitions.
#
#{classes}
BASE_CLASS = #{base_class}
extend CClass::Redirects
# Hand made customizations
#
#{handmade}
end
END_DL_CODE
return dl
end
def to_s
o = ''
o += title "#{@types.length} Types:" + @types.join(" | ") + "\n"
o += title "#{@all_synon.length} Type aliases:"
@all_synon.each { |syn,syn_to|
o += "#{syn} => #{syn_to}\n"
}
o += "\n"
o += title "#{@decls.length} Functions:"
o += @decls.collect { |d| d[:full] }.join("\n")
o += "\n"
o += title "#{@protos.length} Prototypes:"
o += @protos.values.join("\n")
o += "\n"
o += title "#{@enums.length} Enumerations:"
@enums.each { |name,mem|
o += name + "{\n"
mem.each { |k,v|
o += "\t\t#{k} => #{v}\n"
}
o += "}\n"
}
o += "\n"
o += title "#{@structs.length} Structures:"
@structs.each { |name, str|
o += name + "\n"
#~ o += name + '{ '
#~ str.each { |mem|
#~ o += "\t\t#{mem[0]} => #{mem[1]}\n"
#~ }
#~ o += "}\n"
}
o += "\n"
return o
end
def classgraph(k = nil, lev = 0)
if k.nil?
k = @config[:classes][0]
puts title('Class tree')
end
puts "#{''.pad(lev-1,' ')} #{k[:name]} " + ((!k[:wraps].nil?) ? "(#{k[:wraps]})" : '')
@config[:classes].each { |sub|
classgraph(sub, lev+1) if (sub[:child_of] == k[:name])
}
end
def run(config_file, output_file)
begin
puts "Reading and parsing bindings configuration file"
cfg = IO::read(config_file);
cfg = eval(cfg);
@config = cfg
puts "Preprocessing the headers"
run_preprocessor
puts "Parsing the C code"
parse_data
puts "Generating Ruby bindings"
dlcode = create_dl_code
puts "Writing output file"
open(output_file,'w') { |out| out.write(dlcode) }
puts "Done !"
rescue => e
puts e.to_s
puts e.backtrace
return false
end
return true
end
end
#------------------------------------------------------------------------
# You can run this file from the command line to generate your bindings,
# after you have written a config file
#------------------------------------------------------------------------
if __FILE__ == $0
def show_usage
puts <<ENDUSAGE
USAGE: autobindings.rb [OPTIONS] config_file output_file
OPTIONS:
--preprocessor : want to use something different than GCC ?
--preprocessor-params : you can feed it extra parameters.
--headers-path : don't bog down your config file (and gcc) with long paths
--handmade : path to a file that will be inserted verbatim after the generated code
DEBUG OPTIONS:
--debug : traces what we gathered from the C parser
--lines : display all the preprocessed code with line numbers.
--graph : prints a graph of the class hierarchy
ENDUSAGE
end
require 'getoptlong'
h2dl = Autobindings.new
begin
opts = GetoptLong.new(
[ '--preprocessor', GetoptLong::REQUIRED_ARGUMENT ],
[ '--preprocessor-params', GetoptLong::REQUIRED_ARGUMENT ],
[ '--headers-path', GetoptLong::REQUIRED_ARGUMENT ],
[ '--handmade', GetoptLong::REQUIRED_ARGUMENT ],
[ '--debug', GetoptLong::NO_ARGUMENT ],
[ '--lines', GetoptLong::NO_ARGUMENT ],
[ '--graph', GetoptLong::NO_ARGUMENT ]
)
output = nil
opts.each { |opt, arg|
case opt
when '--preprocessor'; h2dl.gcc_cmd = arg.to_s
when '--preprocessor-params'; h2dl.gcc_extra_params = arg.to_s
when '--headers-path'; h2dl.headers_path = arg.to_s
when '--handmade'; h2dl.handmade = arg.to_s
when '--debug'; h2dl.debug = true
when '--lines'; h2dl.debug_lines = true
when '--graph'; h2dl.graph = true
end
}
rescue => err
show_usage
exit -1
end
if ARGV.length < 2
show_usage
exit -1
end
exit -1 if !h2dl.run(ARGV[0], ARGV[1])
end