troessner/reek

View on GitHub
samples/smelly_source/inline.rb

Summary

Maintainability
F
4 days
Test Coverage
#!/usr/local/bin/ruby -w

##
# Ruby Inline is a framework for writing ruby extensions in foreign
# languages.
#
# == SYNOPSIS
#
#   require 'inline'
#   class MyClass
#     inline do |builder|
#       builder.include "<math.h>"
#       builder.c %q{
#         long factorial(int max) {
#           int i=max, result=1;
#           while (i >= 2) { result *= i--; }
#           return result;
#         }
#       }
#     end
#   end
#
# == DESCRIPTION
#
# Inline allows you to write foreign code within your ruby code. It
# automatically determines if the code in question has changed and
# builds it only when necessary. The extensions are then automatically
# loaded into the class/module that defines it.
#
# You can even write extra builders that will allow you to write
# inlined code in any language. Use Inline::C as a template and look
# at Module#inline for the required API.
#
# == PACKAGING
#
# To package your binaries into a gem, use hoe's INLINE and
# FORCE_PLATFORM env vars.
#
# Example:
#
#   rake package INLINE=1
#
# or:
#
#   rake package INLINE=1 FORCE_PLATFORM=mswin32
#
# See hoe for more details.
#

require "rbconfig"
require "digest/md5"
require 'fileutils'
require 'rubygems'

$TESTING = false unless defined? $TESTING

class CompilationError < RuntimeError; end

##
# The Inline module is the top-level module used. It is responsible
# for instantiating the builder for the right language used,
# compilation/linking when needed, and loading the inlined code into
# the current namespace.

module Inline
  VERSION = '3.7.0'

  WINDOZE  = /win(32|64)/ =~ RUBY_PLATFORM
  RUBINIUS = defined? RUBY_ENGINE
  DEV_NULL = (WINDOZE ? 'nul'      : '/dev/null')
  GEM      = (WINDOZE ? 'gem.bat'  : 'gem')
  RAKE     = if WINDOZE then
               'rake.bat'
             elsif RUBINIUS then
               File.join(Gem.bindir, 'rake')
             else
               "#{Gem.ruby} -S rake"
             end


  warn "RubyInline v #{VERSION}" if $DEBUG

  protected

  def self.rootdir
    env = ENV['INLINEDIR'] || ENV['HOME']

    # in case both INLINEDIR and HOME aren't defined, and under Windows
    # default to HOMEDRIVE + HOMEPATH values
    env = ENV['HOMEDRIVE'] + ENV['HOMEPATH'] if env.nil? and WINDOZE

    if env.nil? then
      abort "Define INLINEDIR or HOME in your environment and try again"
    end

    unless defined? @@rootdir and env == @@rootdir and test ?d, @@rootdir then
      rootdir = env
      Dir.mkdir rootdir, 0700 unless test ?d, rootdir
      Dir.assert_secure rootdir
      @@rootdir = rootdir
    end

    @@rootdir
  end

  def self.directory
    directory = File.join(rootdir, ".ruby_inline")
    unless defined? @@directory and directory == @@directory
      @@directory = File.join(self.rootdir, ".ruby_inline")
    end
    Dir.assert_secure directory
    @@directory
  end

  # Inline::C is the default builder used and the only one provided by
  # Inline. It can be used as a template to write builders for other
  # languages. It understands type-conversions for the basic types and
  # can be extended as needed.

  class C

    protected unless $TESTING

    MAGIC_ARITY_THRESHOLD = 15
    MAGIC_ARITY = -1

    @@type_map = {
      'char'          => [ 'NUM2CHR',  'CHR2FIX' ],
      'char *'        => [ 'STR2CSTR', 'rb_str_new2' ],
      'double'        => [ 'NUM2DBL',  'rb_float_new' ],
      'int'           => [ 'F'+'IX2INT',  'INT2FIX' ],
      'long'          => [ 'NUM2INT',  'INT2NUM' ],
      'unsigned int'  => [ 'NUM2UINT', 'UINT2NUM' ],
      'unsigned long' => [ 'NUM2UINT', 'UINT2NUM' ],
      'unsigned'      => [ 'NUM2UINT', 'UINT2NUM' ],
      'VALUE'         => [ '', '' ],
      # Can't do these converters because they conflict with the above:
      # ID2SYM(x), SYM2ID(x), NUM2DBL(x), F\IX2UINT(x)
    }

    def ruby2c(type)
      raise ArgumentError, "Unknown type #{type.inspect}" unless @@type_map.has_key? type
      @@type_map[type].first
    end

    def c2ruby(type)
      raise ArgumentError, "Unknown type #{type.inspect}" unless @@type_map.has_key? type
      @@type_map[type].last
    end

    def strip_comments(src)
      # strip c-comments
      src = src.gsub(%r%\s*/\*.*?\*/%m, '')
      # strip cpp-comments
      src = src.gsub(%r%^\s*//.*?\n%, '')
      src = src.gsub(%r%[ \t]*//[^\n]*%, '')
      src
    end

    def parse_signature(src, raw=false)

      sig = self.strip_comments(src)
      # strip preprocessor directives
      sig.gsub!(/^\s*\#.*(\\\n.*)*/, '')
      # strip {}s
      sig.gsub!(/\{[^\}]*\}/, '{ }')
      # clean and collapse whitespace
      sig.gsub!(/\s+/, ' ')

      unless defined? @types then
        @types = 'void|' + @@type_map.keys.map{|x| Regexp.escape(x)}.join('|')
      end

      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([$4, $1])
          elsif arg != "void" then
            warn "WAR\NING: '#{arg}' not understood"
          end
        end

        arity = args.size
        arity = MAGIC_ARITY if raw

        return {
          'return' => return_type,
          'name'   => function_name,
          'args'   => args,
          'arity'  => arity
        }
      end

      raise SyntaxError, "Can't parse signature: #{sig}"
    end # def parse_signature

    def generate(src, options={})
      options = {:expand_types=>options} unless Hash === options

      expand_types = options[:expand_types]
      singleton = options[:singleton]
      result = self.strip_comments(src)

      signature = parse_signature(src, !expand_types)
      function_name = signature['name']
      method_name = options[:method_name] || function_name
      return_type = signature['return']
      arity = signature['arity']

      raise ArgumentError, "too many arguments" if arity > MAGIC_ARITY_THRESHOLD

      if expand_types then
        prefix = "static VALUE #{function_name}("
        if arity == MAGIC_ARITY then
          prefix += "int argc, VALUE *argv, VALUE self"
        else
          prefix += "VALUE self"
          prefix += signature['args'].map { |arg, type| ", VALUE _#{arg}"}.join
        end
        prefix += ") {\n"
        prefix += signature['args'].map { |arg, type|
          "  #{type} #{arg} = #{ruby2c(type)}(_#{arg});\n"
        }.join

        # replace the function signature (hopefully) with new sig (prefix)
        result.sub!(/[^;\/\"\>]+#{function_name}\s*\([^\{]+\{/, "\n" + prefix)
        result.sub!(/\A\n/, '') # strip off the \n in front in case we added it
        unless return_type == "void" then
          raise SyntaxError, "Couldn't find return statement for #{function_name}" unless
            result =~ /return/
          result.gsub!(/return\s+([^\;\}]+)/) do
            "return #{c2ruby(return_type)}(#{$1})"
          end
        else
          result.sub!(/\s*\}\s*\Z/, "\nreturn Qnil;\n}")
        end
      else
        prefix = "static #{return_type} #{function_name}("
        result.sub!(/[^;\/\"\>]+#{function_name}\s*\(/, prefix)
        result.sub!(/\A\n/, '') # strip off the \n in front in case we added it
      end

      delta = if result =~ /\A(static.*?\{)/m then
                $1.split(/\n/).size
              else
                warn "WAR\NING: Can't find signature in #{result.inspect}\n" unless $TESTING
                0
              end

      file, line = caller[1].split(/:/)
      result = "# line #{line.to_i + delta} \"#{file}\"\n" + result unless $DEBUG and not $TESTING

      @src << result
      @sig[function_name] = [arity,singleton,method_name]

      return result if $TESTING
    end # def generate

    def module_name
      unless defined? @module_name then
        module_name = @mod.name.gsub('::','__')
        md5 = Digest::MD5.new
        @sig.keys.sort_by { |x| x.to_s }.each { |m| md5 << m.to_s }
        @module_name = "Inline_#{module_name}_#{md5.to_s[0,4]}"
      end
      @module_name
    end

    def so_name
      unless defined? @so_name then
        @so_name = "#{Inline.directory}/#{module_name}.#{Config::CONFIG["DLEXT"]}"
      end
      @so_name
    end

    attr_reader :rb_file, :mod
    if $TESTING then
      attr_writer :mod
      attr_accessor :src, :sig, :flags, :libs
    end

    public

    def initialize(mod)
      raise ArgumentError, "Class/Module arg is required" unless Module === mod
      # new (but not on some 1.8s) -> inline -> real_caller|eval
      stack = caller
      meth = stack.shift until meth =~ /in .(inline|test_|setup)/ or stack.empty?
      raise "Couldn't discover caller" if stack.empty?
      real_caller = stack.first
      real_caller = stack[3] if real_caller =~ /\(eval\)/
      real_caller = real_caller.split(/:/, 3)[0..1]
      @real_caller = real_caller.join ':'
      @rb_file = File.expand_path real_caller.first

      @mod = mod
      @src = []
      @inc = []
      @sig = {}
      @flags = []
      @libs = []
      @init_extra = []
      @include_ruby_first = true
    end

    ##
    # Attempts to load pre-generated code returning true if it succeeds.

    def load_cache
      begin
        file = File.join("inline", File.basename(so_name))
        if require file then
          dir = Inline.directory
          warn "WAR\NING: #{dir} exists but is not being used" if test ?d, dir and $VERBOSE
          return true
        end
      rescue LoadError
      end
      return false
    end

    ##
    # Loads the generated code back into ruby

    def load
      require "#{so_name}" or raise LoadError, "require on #{so_name} failed"
    end

    ##
    # Builds the source file, if needed, and attempts to compile it.

    def build
      so_name = self.so_name
      so_exists = File.file? so_name
      unless so_exists and File.mtime(rb_file) < File.mtime(so_name) then

        unless File.directory? Inline.directory then
          warn "NOTE: creating #{Inline.directory} for RubyInline" if $DEBUG
          Dir.mkdir Inline.directory, 0700
        end

        src_name = "#{Inline.directory}/#{module_name}.c"
        old_src_name = "#{src_name}.old"
        should_compare = File.write_with_backup(src_name) do |io|
          if @include_ruby_first
            @inc.unshift "#include \"ruby.h\""
          else
            @inc.push "#include \"ruby.h\""
          end

          io.puts
          io.puts @inc.join("\n")
          io.puts
          io.puts @src.join("\n\n")
          io.puts
          io.puts
          io.puts "#ifdef __cplusplus"
          io.puts "extern \"C\" {"
          io.puts "#endif"
          io.puts "  __declspec(dllexport)" if WINDOZE
          io.puts "  void Init_#{module_name}() {"
          io.puts "    VALUE c = rb_cObject;"

          # TODO: use rb_class2path
          # io.puts "    VALUE c = rb_path2class(#{@mod.name.inspect});"
          io.puts @mod.name.split("::").map { |n|
            "    c = rb_const_get(c,rb_intern(\"#{n}\"));"
          }.join("\n")

          @sig.keys.sort.each do |name|
            arity, singleton, method_name = @sig[name]
            if singleton then
              io.print "    rb_define_singleton_method(c, \"#{method_name}\", "
            else
              io.print "    rb_define_method(c, \"#{method_name}\", "
            end
            io.puts  "(VALUE(*)(ANYARGS))#{name}, #{arity});"
          end
          io.puts @init_extra.join("\n") unless @init_extra.empty?

          io.puts
          io.puts "  }"
          io.puts "#ifdef __cplusplus"
          io.puts "}"
          io.puts "#endif"
          io.puts
        end

        # recompile only if the files are different
        recompile = true
        if so_exists and should_compare and
            FileUtils.compare_file(old_src_name, src_name) then
          recompile = false

          # Updates the timestamps on all the generated/compiled files.
          # Prevents us from entering this conditional unless the source
          # file changes again.
          t = Time.now
          File.utime(t, t, src_name, old_src_name, so_name)
        end

        if recompile then

          hdrdir = %w(srcdir archdir rubyhdrdir).map { |name|
            Config::CONFIG[name]
          }.find { |dir|
            dir and File.exist? File.join(dir, "/ruby.h")
          } or abort "ERROR: Can't find header dir for ruby. Exiting..."

          flags = @flags.join(' ')
          libs  = @libs.join(' ')

          config_hdrdir = if RUBY_VERSION > '1.9' then
                            "-I #{File.join hdrdir, RbConfig::CONFIG['arch']}"
                          else
                            nil
                          end

          cmd = [ Config::CONFIG['LDSHARED'],
                  flags,
                  Config::CONFIG['CCDLFLAGS'],
                  Config::CONFIG['CFLAGS'],
                  '-I', hdrdir,
                  config_hdrdir,
                  '-I', Config::CONFIG['includedir'],
                  "-L#{Config::CONFIG['libdir']}",
                  '-o', so_name.inspect,
                  File.expand_path(src_name).inspect,
                  libs,
                  crap_for_windoze ].join(' ')

          # TODO: remove after osx 10.5.2
          cmd += ' -flat_namespace -undefined suppress' if
            RUBY_PLATFORM =~ /darwin9\.[01]/
          cmd += " 2> #{DEV_NULL}" if $TESTING and not $DEBUG

          warn "Building #{so_name} with '#{cmd}'" if $DEBUG
          result = `#{cmd}`
          warn "Output:\n#{result}" if $DEBUG
          if $? != 0 then
            bad_src_name = src_name + ".bad"
            File.rename src_name, bad_src_name
            raise CompilationError, "error executing #{cmd.inspect}: #{$?}\nRenamed #{src_name} to #{bad_src_name}"
          end

          # NOTE: manifest embedding is only required when using VC8 ruby
          # build or compiler.
          # Errors from this point should be ignored if Config::CONFIG['arch']
          # (RUBY_PLATFORM) matches 'i386-mswin32_80'
          if WINDOZE and RUBY_PLATFORM =~ /_80$/ then
            Dir.chdir Inline.directory do
              cmd = "mt /manifest lib.so.manifest /outputresource:so.dll;#2"
              warn "Embedding manifest with '#{cmd}'" if $DEBUG
              result = `#{cmd}`
              warn "Output:\n#{result}" if $DEBUG
              if $? != 0 then
                raise CompilationError, "error executing #{cmd}: #{$?}"
              end
            end
          end

          warn "Built successfully" if $DEBUG
        end

      else
        warn "#{so_name} is up to date" if $DEBUG
      end # unless (file is out of date)
    end # def build

    ##
    # Returns extra compilation flags for windoze platforms. Ugh.

    def crap_for_windoze
      # gawd windoze land sucks
      case RUBY_PLATFORM
      when /mswin32/ then
        " -link /LIBPATH:\"#{Config::CONFIG['libdir']}\" /DEFAULTLIB:\"#{Config::CONFIG['LIBRUBY']}\" /INCREMENTAL:no /EXPORT:Init_#{module_name}"
      when /mingw32/ then
        " -Wl,--enable-auto-import -L#{Config::CONFIG['libdir']} -lmsvcrt-ruby18"
      when /i386-cygwin/ then
        ' -L/usr/local/lib -lruby.dll'
      else
        ''
      end
    end

    ##
    # Adds compiler options to the compiler command line.  No
    # preprocessing is done, so you must have all your dashes and
    # everything.

    def add_compile_flags(*flags)
      @flags.push(*flags)
    end

    ##
    # Adds linker flags to the link command line.  No preprocessing is
    # done, so you must have all your dashes and everything.

    def add_link_flags(*flags)
      @libs.push(*flags)
    end

    ##
    # Adds custom content to the end of the init function.

    def add_to_init(*src)
      @init_extra.push(*src)
    end

    ##
    # Registers C type-casts +r2c+ and +c2r+ for +type+.

    def add_type_converter(type, r2c, c2r)
      warn "WAR\NING: overridding #{type} on #{caller[0]}" if @@type_map.has_key? type
      @@type_map[type] = [r2c, c2r]
    end

    ##
    # Maps a ruby constant to C (with the same name)

    def map_ruby_const(*names)
      names.each do |name|
        self.prefix "static VALUE #{name};"
        self.add_to_init "    #{name} = rb_const_get(c, rb_intern(#{name.to_s.inspect}));"
      end
    end

    ##
    # Maps a C constant to ruby (with the same
    # name). +names_and_types+ is a hash that maps the name of the
    # constant to its C type.

    def map_c_const(names_and_types)
      names_and_types.each do |name, typ|
        self.add_to_init "    rb_define_const(c, #{name.to_s.inspect}, #{c2ruby(typ.to_s)}(#{name}));"
      end
    end

    ##
    # Adds an include to the top of the file. Don't forget to use
    # quotes or angle brackets.

    def include(header)
      @inc << "#include #{header}"
    end

    ##
    # Specifies that the the ruby.h header should be included *after* custom
    # header(s) instead of before them.

    def include_ruby_last
      @include_ruby_first = false
    end

    ##
    # Adds any amount of text/code to the source

    def prefix(code)
      @src << code
    end

    ##
    # Adds a C function to the source, including performing automatic
    # type conversion to arguments and the return value. The Ruby
    # method name can be overridden by providing method_name. Unknown
    # type conversions can be extended by using +add_type_converter+.

    def c src, options = {}
      options = {
        :expand_types => true,
      }.merge options
      self.generate src, options
    end

    ##
    # Same as +c+, but adds a class function.

    def c_singleton src, options = {}
      options = {
        :expand_types => true,
        :singleton    => true,
      }.merge options
      self.generate src, options
    end

    ##
    # Adds a raw C function to the source. This version does not
    # perform any type conversion and must conform to the ruby/C
    # coding conventions.  The Ruby method name can be overridden
    # by providing method_name.

    def c_raw src, options = {}
      self.generate src, options
    end

    ##
    # Same as +c_raw+, but adds a class function.

    def c_raw_singleton src, options = {}
      options = {
        :singleton => true,
      }.merge options
      self.generate src, options
    end

  end # class Inline::C
end # module Inline

class Module

  ##
  # options is a hash that allows you to pass extra data to your
  # builder.  The only key that is guaranteed to exist is :testing.

  attr_reader :options

  ##
  # Extends the Module class to have an inline method. The default
  # language/builder used is C, but can be specified with the +lang+
  # parameter.

  def inline(lang = :C, options={})
    case options
    when TrueClass, FalseClass then
      warn "WAR\NING: 2nd argument to inline is now a hash, changing to {:testing=>#{options}}" unless options
      options = { :testing => options  }
    when Hash
      options[:testing] ||= false
    else
      raise ArgumentError, "BLAH"
    end

    builder_class = begin
                      Inline.const_get(lang)
                    rescue NameError
                      require "inline/#{lang}"
                      Inline.const_get(lang)
                    end

    @options = options
    builder = builder_class.new self

    yield builder

    unless options[:testing] then
      unless builder.load_cache then
        builder.build
        builder.load
      end
    end
  end
end

class File

  ##
  # Equivalent to +File::open+ with an associated block, but moves
  # any existing file with the same name to the side first.

  def self.write_with_backup(path) # returns true if file already existed

    # move previous version to the side if it exists
    renamed = false
    if test ?f, path then
      renamed = true
      File.rename path, path + ".old"
    end

    File.open(path, "w") do |io|
      yield(io)
    end

    return renamed
  end
end # class File

class Dir

  ##
  # +assert_secure+ checks that if a +path+ exists it has minimally
  # writable permissions. If not, it prints an error and exits. It
  # only works on +POSIX+ systems. Patches for other systems are
  # welcome.

  def self.assert_secure(path)
    mode = File.stat(path).mode
    unless ((mode % 01000) & 0022) == 0 then
      if $TESTING then
        raise SecurityError, "Directory #{path} is insecure"
      else
        abort "#{path} is insecure (#{'%o' % mode}). It may not be group or world writable. Exiting."
      end
    end
  rescue Errno::ENOENT
    # If it ain't there, it's certainly secure
  end
end