--Apple-Mail-14--1046285326 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=US-ASCII; delsp=yes; format=flowed The following attachment, when run, shows the following behavior: > % ruby -v ./add_event_hook_weirdness.rb > ruby 1.8.3 (2005-06-17) [powerpc-darwin8.1.0] > ./add_event_hook_weirdness.rb:620:in `hash': undefined method > `hash' for #<MyMod:0x32c0c4> (NoMethodError) > from ./add_event_hook_weirdness.rb:620:in `y' > from ./add_event_hook_weirdness.rb:626 > from ./add_event_hook_weirdness.rb:625:in `times' > from ./add_event_hook_weirdness.rb:625 MyMod is a module, mixed in to MyClass via include. MyClass has one method (y) and it calls the method (x) included from the module. The event hook that we define takes the id and the klass, puts them in an array, and uses that as a hash key. This, in turn, tries to call hash on the instance of MyMod that was created when MyClass included into it (via include_class_new?). We've tried our best to strip everything down to a 1 file repro with no dependencies outside of standard ruby. As a result, we had to include some of RubyInline, which makes the file bigger. We added tags ("HERE") to mark the start of the real code and the line we think is the offender. We added one extra feature to help debugging. If you define an environment variable "HASH" it'll print out the process ID and sleep instead of just raising NoMethodError. > % HASH=1 ruby -v ./add_event_hook_weirdness.rb > ruby 1.8.3 (2005-06-17) [powerpc-darwin8.1.0] > MyMod#hash invoked from ./add_event_hook_weirdness.rb:620:in `y' > connect to 10702 now... sleeping > C-c C-c./add_event_hook_weirdness.rb:609:in `sleep': Interrupt > from ./add_event_hook_weirdness.rb:609:in `hash' > from ./add_event_hook_weirdness.rb:620:in `y' > from ./add_event_hook_weirdness.rb:626 > from ./add_event_hook_weirdness.rb:625:in `times' > from ./add_event_hook_weirdness.rb:625 We tried very hard to debug this ourselves, but our internals- debugging-fu is not strong enough. --Apple-Mail-14--1046285326 Content-Transfer-Encoding: 7bit Content-Type: text/x-ruby-script; x-unix-mode=0744; name="add_event_hook_weirdness.rb" Content-Disposition: attachment; filename=add_event_hook_weirdness.rb #!/usr/local/bin/ruby -w # To find the real code under test, search for "HERE" (twice) require 'singleton' require "rbconfig" require "digest/md5" require 'ftools' require 'fileutils' $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 = '-1' $stderr.puts "RubyInline v #{VERSION}" if $DEBUG protected def self.rootdir env = ENV['INLINEDIR'] || ENV['HOME'] unless defined? @@rootdir and env == @@rootdir and test ?d, @@rootdir then rootdir = ENV['INLINEDIR'] || ENV['HOME'] 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 and test ?d, @@directory then unless File.directory? directory then $stderr.puts "NOTE: creating #{directory} for RubyInline" if $DEBUG Dir.mkdir directory, 0700 end Dir.assert_secure directory @@directory = directory end @@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 = 2 MAGIC_ARITY = -1 @@type_map = { 'char' => [ 'NUM2CHR', 'CHR2FIX' ], 'char *' => [ 'STR2CSTR', 'rb_str_new2' ], 'double' => [ 'NUM2DBL', 'rb_float_new' ], 'int' => [ 'FIX2INT', 'INT2FIX' ], 'long' => [ 'NUM2INT', 'INT2NUM' ], 'unsigned int' => [ 'NUM2UINT', 'UINT2NUM' ], 'unsigned long' => [ 'NUM2UINT', 'UINT2NUM' ], 'unsigned' => [ 'NUM2UINT', 'UINT2NUM' ], # Can't do these converters because they conflict with the above: # ID2SYM(x), SYM2ID(x), NUM2DBL(x), FIX2UINT(x) } def ruby2c(type) raise ArgumentError, "Unknown type #{type}" unless @@type_map.has_key? type @@type_map[type].first end def c2ruby(type) raise ArgumentError, "Unknown type #{type}" unless @@type_map.has_key? type @@type_map[type].last end def strip_comments(src) # strip c-comments src = src.gsub(/\s*(?:(?:\/\*)(?:(?:(?!\*\/)[\s\S])*)(?:\*\/))/, '') # strip cpp-comments src.gsub!(/\s*(?:\/\*(?:(?!\*\/)[\s\S])*\*\/|\/\/[^\n]*\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+/, ' ') types = 'void|VALUE|' + @@type_map.keys.map{|x| Regexp.escape(x)}.join('|') 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+(\w+)\s*$/ =~ arg if /(((#{types})\s*\*?)+)\s+(\w+)\s*$/ =~ arg then args.push([$4, $1]) elsif arg != "void" then $stderr.puts "WARNING: '#{arg}' not understood" end end arity = args.size arity = -1 if arity > MAGIC_ARITY_THRESHOLD or 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={}) if not Hash === options then options = {:expand_types=>options} end expand_types = options[:expand_types] singleton = options[:singleton] result = self.strip_comments(src) signature = parse_signature(src, !expand_types) function_name = signature['name'] return_type = signature['return'] arity = signature['arity'] 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" signature['args'].each do |arg, type| prefix += ", VALUE _#{arg}" end end prefix += ") {\n" if arity == MAGIC_ARITY then count = 0 signature['args'].each do |arg, type| prefix += " #{type} #{arg} = #{ruby2c(type)}(argv[#{count}]);\n" count += 1 end else signature['args'].each do |arg, type| prefix += " #{type} #{arg} = #{ruby2c(type)}(_#{arg});\n" end end # 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 "WARNING: 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 @src << result @sig[function_name] = [arity,singleton] 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 attr_accessor :mod, :src, :sig, :flags, :libs if $TESTING 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/ real_caller = stack.first real_caller = stack[3] if real_caller =~ /\(eval\)/ @real_caller = real_caller.split(/:/).first @rb_file = File.expand_path(@real_caller) @mod = mod @src = [] @sig = {} @flags = [] @libs = [] 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 "WARNING: #{dir} exists but is not being used" if test ?d, dir 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) src_name = "#{Inline.directory}/#{module_name}.c" old_src_name = "#{src_name}.old" should_compare = File.write_with_backup(src_name) do |io| io.puts io.puts "#include \"ruby.h\"" 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 " void Init_#{module_name}() {" io.puts " VALUE c = rb_cObject;" # TODO: use rb_class2path @mod.name.split("::").each do |n| io.puts " c = rb_const_get_at(c,rb_intern(\"#{n}\"));" end @sig.keys.sort.each do |name| arity, singleton = @sig[name] if singleton then io.print " rb_define_singleton_method(c, \"#{name}\", " else io.print " rb_define_method(c, \"#{name}\", " end io.puts "(VALUE(*)(ANYARGS))#{name}, #{arity});" end 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 File::compare(old_src_name, src_name, $DEBUG) 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 # extracted from mkmf.rb srcdir = Config::CONFIG["srcdir"] archdir = Config::CONFIG["archdir"] if File.exist? archdir + "/ruby.h" then hdrdir = archdir elsif File.exist? srcdir + "/ruby.h" then hdrdir = srcdir else $stderr.puts "ERROR: Can't find header files for ruby. Exiting..." exit 1 end flags = @flags.join(' ') flags += " #{$INLINE_FLAGS}" if defined? $INLINE_FLAGS# DEPRECATE libs = @libs.join(' ') libs += " #{$INLINE_LIBS}" if defined? $INLINE_LIBS # DEPRECATE cmd = "#{Config::CONFIG['LDSHARED']} #{flags} #{Config::CONFIG['CFLAGS']} -I #{hdrdir} -o #{so_name} #{src_name} #{libs}" case RUBY_PLATFORM when /mswin32/ then cmd += " -link /INCREMENTAL:no /EXPORT:Init_#{module_name}" when /i386-cygwin/ then cmd += ' -L/usr/local/lib -lruby.dll' end cmd += " 2> /dev/null" if $TESTING $stderr.puts "Building #{so_name} with '#{cmd}'" if $DEBUG `#{cmd}` if $? != 0 then bad_src_name = src_name + ".bad" File.rename src_name, bad_src_name raise CompilationError, "error executing #{cmd}: #{$?}\nRenamed #{src_name} to #{bad_src_name}" end $stderr.puts "Built successfully" if $DEBUG end else $stderr.puts "#{so_name} is up to date" if $DEBUG end # unless (file is out of date) end # def build ## # 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 ## # Registers C type-casts +r2c+ and +c2r+ for +type+. def add_type_converter(type, r2c, c2r) $stderr.puts "WARNING: overridding #{type}" if @@type_map.has_key? type @@type_map[type] = [r2c, c2r] end ## # Adds an include to the top of the file. Don't forget to use # quotes or angle brackets. def include(header) @src << "#include #{header}" 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. Unknown type # conversions can be extended by using +add_type_converter+. def c src self.generate(src,:expand_types=>true) end ## # Same as +c+, but adds a class function. def c_singleton src self.generate(src,:expand_types=>true,:singleton=>true) 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. def c_raw src self.generate(src) end ## # Same as +c_raw+, but adds a class function. def c_raw_singleton src self.generate(src, :singleton=>true) 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 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 to see that +path+ exists and 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 $stderr.puts "#{path} is insecure (#{sprintf('%o', mode)}), needs 0700 for perms. Exiting." exit 1 end end end end # HERE is the start of the real code under test class BadOptimizer include Singleton @@data = Hash.new(0) def self.start_optimizing self.instance.add_event_hook end ############################################################ # Inlined Methods: inline(:C) do |builder| builder.add_type_converter("rb_event_t", '', '') builder.add_type_converter("NODE *", '', '') builder.add_type_converter("ID", '', '') builder.include '"ruby.h"' builder.include '"node.h"' builder.prefix "static VALUE optimizer_klass = Qnil; static VALUE data = Qnil;" builder.c_raw <<-'EOF' static void prof_event_hook(rb_event_t event, NODE *node, VALUE self, ID mid, VALUE klass) { static int optimizing = 0; if (NIL_P(optimizer_klass)) optimizer_klass = rb_path2class("BadOptimizer"); if (NIL_P(data)) data = rb_cv_get(optimizer_klass, "@@data"); if (optimizing) return; optimizing++; switch (event) { case RUBY_EVENT_CALL: { VALUE signature; signature = rb_ary_new2(2); rb_ary_store(signature, 0, klass); rb_ary_store(signature, 1, ID2SYM(mid)); rb_hash_aref(data, signature); // HERE is the problem!!! } break; } optimizing--; } EOF builder.c <<-'EOF' void add_event_hook() { rb_add_event_hook(prof_event_hook, RUBY_EVENT_CALL); } EOF end end BadOptimizer::start_optimizing $data = Hash.new(0) module MyMod def hash $stderr.puts "MyMod#hash invoked from #{Kernel.caller.first}" $stderr.puts "connect to #{$$} now... sleeping" Kernel.sleep end if ENV.has_key? 'HASH' def x 42 end end class MyClass include MyMod def y x end end c = MyClass.new 1000.times do c.y end --Apple-Mail-14--1046285326--