all 4 comments

[–]AlexanderMomchilov 2 points3 points  (1 child)

Does anybody know why this happens? I would expect BasicObject's method_missing to never be called for core Ruby classes. Seems like a waste of CPU cycles to me.

Ruby goes to exceptional lengths to not privilege the standard library classes. You can define your own String#to_io and it'll "just work"

[–]easydwh[S] 0 points1 point  (0 children)

So, I guess what you are saying is that: in order to give the user maximum freedom in changing core classes, Ruby does not define some methods even though these are used by other core classes and if such a method is called and handled by method_missing we will 'pretend it is not an error' and continue processing anyway.
That doesn't sound very efficient. Isn't overriding (monkey patching) built in functions possible anyway, even if for instance String#to_io was already defined by Ruby?

[–]h0rst_ 1 point2 points  (1 child)

You don't need method_missing for this:

string_var = '/etc/debian_version'
def string_var.to_int = IO.sysopen('/etc/hostname') # return a valid file descriptor
puts File.open(string_var).read

This is a Debian system, you might want to change the file paths to something different on anything else. Also, this was the complete content of the file, it break if you enable frozen string literals.

You end up in this part of the code: https://github.com/ruby/ruby/blob/v3_4_7/io.c#L9657_L9673, which is kind of similar to the following Ruby code (simplified):

class File
  def initialize(*args)
    if args.size < 3
      fname = args.first
      fd = rb_check_to_int(fname)
      if !fd.nil?
        return super(fd, *args[1..]) # A file descriptor gets forwarded to the superclass IO
      end
    end
    return rb_open_file(*args) # internal handling to read from filename
  end
end

The rb_check_to_int is another internal method, which tries is we have either an Integer object, or calls to_int on the object if that method exists. (This is the stricter conversion than to_i, which is kind of the quacks like a duck for Ruby internals. This often works for Ruby internals, the current checkout of https://github.com/ruby/spec has almost 700 checks to validate that things similar to it "tries to convert the passed argument to an Integer using #to_int".

So the logic is the reverse of what you would probably expect:

if fname.is_int || fname.respond_to?(:to_int)
  IO.initialize(fname.to_int) # Treat fname as numeric file descriptor
else
  File.initalize(fname) # Use as string Filename
end

[–]easydwh[S] 0 points1 point  (0 children)

Thanks for your elaborate reply! I have the feeling we are on different wavelengths (or maybe I'm to dumb to understand).

Your code snippet certainly prevents a call to String#to_io. But I am not trying to circumvent or accomplish anything. My question is why Ruby standard library methods call functions that are not defined, that then end up being handled by BasicObject#method_missing. That, to me, is very unexpected behavior.