Ontem, acredito que esbarrei feio num bug do Ruby 1.8 (não sei se outras versões estão com o mesmo problema). Para encurtar a longa história, estava mexendo num código usando TheRubyRacer semelhante ao seguinte (nota: se você não entender o código a seguir, vale a pena dar uma olhada em meu post anterior):
class UmaClasse
def method_missing(method, *args, &b)
puts "Método #{method} chamado!"
...
end
end
require 'v8'
context = V8::Context.new
context['classe'] = UmaClasse.new
p context.eval('classe.um_metodo')
p context.eval('classe.um_metodo = 10')
O que eu esperava, é que na linha 11, fosse impressa a mensagem “Método um_metodo chamado!”, porém o que aconteceu não foi bem isso. Quando olhei para a documentação da biblioteca, descobri que ela expõe apenas os métodos públicos por padrão, agora COMO ela fazia isso… isso já é outro problema. Então, num primeiro momento, resolvi remover todos os métodos da classe UmaClasse, e ver o que acontecia:
class UmaClasse
instance_methods.each do |m|
send :undef_method, m
end
end
#O resto do código é igual
Ok, descobri dois problemas, um no método “tap”, e outro indicando que remover os métodos “id” e “send” pode ser danoso. Resolvi então manter estes métodos
class UmaClasse
instance_methods.each do |m|
next if ['tap', '<strong>send</strong>', '<strong>id</strong>'].include?(m)
send :undef_method, m
end
end
Ok, consegui uma mensagem mais clara: “Undefined Method ‘public_methods'”. Então, a biblioteca TheRubyRacer usa o public_methods para assegurar que o método em questão é público. Então, bastaria sobrescrever o método “public_methods”, e retornar um objeto qualquer que respondesse por “include?”, e retornasse “true”. Dessa forma, sempre que a biblioteca TheRubyRacer chamasse o public_methods, e verificasse se public_methods.include?(), retornaria true. Então, tudo daria certo… acho. O código ficou assim (para evitar duplicações, vou mostrar apenas a classe UmaClasse):
class UmaClasse
def method_missing(method, *args, &b)
puts "Método #{method} chamado!"
...
end
def public_methods(defined=false)
stub = []
def stub.include?(other); return true; end
return stub
end
end
Ops… isso não funciona. Desistindo, resolvi abrir o código-fonte da biblioteca, buscando por “public_methods”, e vi que ele chama a função rb_ary_includes para definir se uma determinada string está dentro do array. A definição da função C é:
rb_ary_includes(ary, item)
VALUE ary;
VALUE item;
{
long i;
<pre><code>for (i=0; i&amp;lt;RARRAY(ary)-&amp;gt;len; i++) {
if (rb_equal(RARRAY(ary)-&amp;gt;ptr[i], item)) {
return Qtrue;
}
}
return Qfalse;
</code></pre>
}
Logo, ele lista objeto por objeto, e vê se ele é igual. Bom… então, a coisa complica um pouco mais: precisamos, de alguma forma, retornar um array com um objeto que SEMPRE é igual a qualquer coisa. O código é o seguinte:
class UmaClasse
...
def public_methods(defined=false)
stub = '' #Uma string qualquer
def stub.==(other); return true; end
return [stub]
end
end
Whoa!!! Funciona, com a mensagem “undefined method algo' for classUmaClasse’ (NameError)”. Class? eu esperava que ele retornasse uma instância!!! Isso também significa que, meu “method_missing” não funcionou. Tentar definir o “method_missing” no contexto da classe também não funcionou – provavelmente, algum bug do Ruby, que impede que funções chamadas pela API C do Ruby, com rb_funcall2, entendam o “method_missing” apresentado no Ruby…
Bom, então encurtando a história: basicamente, o que eu queria fazer era que, de alguma forma, quando no contexto do V8/Javascript eu chamasse um método tipo “classe.valor = 10”, ele automaticamente criasse essa propriedade “valor”, e atribuísse um valor “10” para ele. Aí, tanto em chamadas Javascript como em Ruby, o método “classe.valor” deveria retornar 10. A idéia inicial era interceptar o method_missing e criar as propriedades dinamicamente, mas como isso não seria possível devido a esse comportamento estranho, ao invés de interceptar o method_missing, eu interceptaria o “public_methods”, e quando ele tentasse usar o “==” de cada um dos elementos, aí eu criaria o método. No fim, o código é:
class UmaClasse
def public_methods(defined=false)
me = self #Preciso disso para manter o contexto
stub = ''
metaclass = class << stub; self; end #Preciso disso para declarar o método == apenas nesta instância
metaclass.send :define_method, :== do |other| #Deve ser com bloco, para manter o escopo do "me"
other = other.chop if other.end_with?("=")
me.class.send :attr_accessor, other
true
end
return [stub]
end
end
require 'v8'
context = V8::Context.new
context['classe'] = classe = UmaClasse.new
p context.eval('classe.um_metodo') #nil
p context.eval('classe.um_metodo = 10') #10
p context.eval('classe.um_metodo') #10
p classe.um_metodo #10
Claro, mais tratamentos são necessários, mas o importante é que, mesmo esbarrando em um possível bug no interpretador Ruby, ainda assim foi possível implementar o comportamento desejado. Com isso, cada vez mais a integração entre Javascript e Ruby parece interessante!