module Inliner DEV_NULL = if Config::CONFIG['target_os'] =~ /mswin|mingw/ 'nul' else '/dev/null' end LIB_EXT = if Config::CONFIG['target_os'] =~ /darwin/ '.dylib' elsif Config::CONFIG['target_os'] =~ /mswin|mingw/ '.dll' else '.so' end C_TO_FFI = { 'void' => :void, 'char' => :char, 'unsigned char' => :uchar, 'int' => :int, 'unsigned int' => :uint, 'long' => :long, 'unsigned long' => :ulong, 'float' => :float, 'double' => :double, } @@__inliner_directory = File.expand_path(File.join('~/', '.ffi-inliner')) class << self def directory @@__inliner_directory end end class FilenameManager def initialize(mod, code) @mod = mod.name.gsub('::', '__') @code = code end def cached? exists? end def exists? File.exists?(c_fn) end def base_fn File.join(Inliner.directory, "#{@mod}_#{(Digest::MD5.new << @code).to_s[0, 4]}") end %w(c rb log).each do |ext| define_method("#{ext}_fn") { "#{base_fn}.#{ext}" } end def so_fn "#{base_fn}#{LIB_EXT}" end end module Compilers class Compiler attr_reader :progname def self.check_and_create(fm = nil, libraries = nil) compiler = new(fm, libraries) unless compiler.exists? raise "Can't find compiler #{compiler.class}" else compiler end end def initialize(fm = nil, libraries = nil) @fm = fm @libraries = libraries @progname = cmd.split.first end def compile raise "Compile error! See #{@fm.log_fn}" unless system(cmd) end private def libs @libraries.inject("") { |str, lib| str << "-l#{lib} " } if @libraries end end class GCC < Compiler def exists? IO.popen("#{@progname} 2>&1") { |f| f.gets } ? true : false end def ldshared if Config::CONFIG['target_os'] =~ /darwin/ 'gcc -dynamic -bundle -fPIC' else 'gcc -shared -fPIC' end end def cmd "#{ldshared} #{libs} -o \"#{@fm.so_fn}\" \"#{@fm.c_fn}\" 2>\"#{@fm.log_fn}\"" end end class GPlusPlus < GCC def ldshared if Config::CONFIG['target_os'] =~ /darwin/ 'g++ -dynamic -bundle -fPIC' else 'g++ -shared -fPIC' end end end class TCC < Compiler def exists? IO.popen("#{@progname}") { |f| f.gets } ? true : false end def cmd if Config::CONFIG['target_os'] =~ /mswin|mingw/ "tcc -rdynamic -shared #{libs} -o \"#{@fm.so_fn}\" \"#{@fm.c_fn}\" 2>\"#{@fm.log_fn}\"" else "tcc -shared #{libs} -o \"#{@fm.so_fn}\" \"#{@fm.c_fn}\" 2>\"#{@fm.log_fn}\"" end end end end class Builder attr_reader :code, :compiler def initialize(mod, code = "", options = {}) make_pointer_types @mod = mod @code = code @sig = [parse_signature(@code)] unless @code.empty? options = { :use_compiler => Compilers::GCC }.merge(options) @compiler = options[:use_compiler] end def map(type_map) @types.merge!(type_map) end def include(fn, options = {}) options[:quoted] ? @code << "#include \"#{fn}\"\n" : @code << "#include <#{fn}>\n" end def library(*libraries) (@libraries ||= []).concat(libraries) end def c(code) (@sig ||= []) << parse_signature(code) @code << (@compiler == Compilers::GPlusPlus ? "extern \"C\" {\n#{code}\n}" : code ) end def c_raw(code) @code << code end def use_compiler(compiler) @compiler = compiler end def struct(ffi_struct) @code << "typedef struct {" ffi_struct.layout.fields.each do |field| @code << "#{field} #{field.name};\n" end @code << "} #{ffi_struct.class.name}" end def build @fm = FilenameManager.new(@mod, @code) @compiler = @compiler.check_and_create(@fm, @libraries) unless @fm.cached? write_files(@code, @sig) @compiler.compile @mod.instance_eval generate_ffi(@sig) else @mod.instance_eval(File.read(@fm.rb_fn)) end end private def make_pointer_types @types = C_TO_FFI.dup C_TO_FFI.each_key do |k| @types["#{k} *"] = :pointer end end def cached?(name, code) File.exists?(cname(name, code)) end def to_ffi_type(c_type) @types[c_type] end # Based on RubyInline code by Ryan Davis # Copyright (c) 2001-2007 Ryan Davis, Zen Spider Software def strip_comments(code) # strip c-comments src = code.gsub(%r%\s*/\*.*?\*/%m, '') # strip cpp-comments src = src.gsub(%r%^\s*//.*?\n%, '') src = src.gsub(%r%[ \t]*//[^\n]*%, '') src end # Based on RubyInline code by Ryan Davis # Copyright (c) 2001-2007 Ryan Davis, Zen Spider Software def parse_signature(code) sig = strip_comments(code) # strip preprocessor directives sig.gsub!(/^\s*\#.*(\\\n.*)*/, '') # strip {}s sig.gsub!(/\{[^\}]*\}/, '{ }') # clean and collapse whitespace sig.gsub!(/\s+/, ' ') # types = 'void|int|char|char\s\*|void\s\*' types = @types.keys.map{|x| Regexp.escape(x)}.join('|') sig = sig.gsub(/\s*\*\s*/, ' * ').strip if /(#{types})\s*(\w+)\s*\(([^)]*)\)/ =~ sig then return_type, function_name, arg_string = $1, $2, $3 args = [] arg_string.split(',').each do |arg| # helps normalize into 'char * varname' form arg = arg.gsub(/\s*\*\s*/, ' * ').strip if /(((#{types})\s*\*?)+)\s+(\w+)\s*$/ =~ arg then args.push($1) elsif arg != "void" then warn "WAR\NING: '#{arg}' not understood" end end arity = args.size return { 'return' => return_type, 'name' => function_name, 'args' => args, 'arity' => arity } end raise SyntaxError, "Can't parse signature: #{sig}" end def generate_ffi(sig) ffi_code = <