Monday, July 12, 2010

instance_eval weirdness in ruby

I've been doing a lot of meta-programming in ruby recently, and ruby people are always saying eval is dangerious, or that using it is a sign you don't know what you are doing... but when someone questions them they can't give a straight answer...

well, I just found one but it's about perl (and page two, which is more direct)

the link refers specifically to storing the name of a variable in a variable, the problem being that it can create collisions.

in perl:
$value refers to the value of the variable 'value'
$$value refers to the value of the variable that is named 'value'.

basically anything could end up in the variable, and if you change the value of that named variable you could change something important. particularity if it's an important variable that's core to the language like $0 or __FILE__. It sounds like this is a lot more dangerous in Perl, because variables are global by default. to define a local variable you have to say 'my $value'
no wonder perl people are against variable names stored in variables.

basically, it's about scope. ruby doesn't have this insane global variable by default feature.

but either way, if you only doing something boring like dynamicially deciding which variable your interested in, you can use hashes.

lets use eval for cool stuff!

like making some code in one place and calling it in other contexts,

or allowing users to run arbitrary code on your web server!

but seriously,

you can still run into name collision problems. one I've just discovered (and fixed) today is as follows.

say you want to define some code and run it in another context:

class Context
attr_accessor :one,:two
end

def add (a,b)
puts "#{a} + #{b} = #{a+b}"
end

def context_a(l,code)
puts "context_a"
l.instance_eval(code)
end
def context_b(l,code)
puts "context_b"
one = 11
two = 22
l.instance_eval(code)
end
def context_c(l,code)
puts "context_c.1"
begin
l.instance_eval(code)
rescue Exception => e
puts e.message
end
puts "context_c.2"
one = 101
two = 202
l.instance_eval(code)
end

l = Context.new
l.one = 1
l.two = 2
code = "add one,two"

context_a(l,code)
context_b(l,code)
context_c(l,code)

so what will happen?
the eval will call add and interpret the meaning of one and two from the context of the instance of Context stored in l, right?

well... not quite. All that eval_instance does is change the value of 'self' while it evaluates the code string. The variables in the local context are still visible, as if the current function was now inside the receiver of instance_eval.

here is the output:

context_a
1 + 2 = 3
context_b
11 + 22 = 33
context_c.1
undefined method `+' for nil:NilClass
context_c.2
101 + 202 = 303
Whats happening is the methods of Context declared in each function are over written by the local variables in context_b and context_c. context_c is even more interesting, because here the local variable haven't even been initialized yet. however they already are already assigned nil at the start of the method!



if you don't know what is in the code you will be evaluating is yet, or then potentially anything could come through and it might collide, producing unexpected errors which will be difficult to diagnose.

However, this problem is easily avoided.

o get a clean call of instance_eval you must call it from a function with a clean local context.
for example, adding the following function to Context and calling that instead will fix the problem.
def clean_instance_eval(code)
eval code
end
you could monkeypatch instance_eval, but then there might be some bit of code which depended on the weird behaviour of eval, and that monkeypatch is no longer bug compatible!

also note that a variable/method named 'code' within code will collide with the context of clean_instance_eval. you could parse code and escape any occurences of code with, but it is much more convienent to just name the code variable to something highly unlikely to be used as a variable for example append a magic number: code_324792523532 or do_not_name_variable_this.

Anyone naming a variable do_not_name_variable_this is looking for trouble, so they deserve it!


No comments:

Post a Comment