Traçando a Execução em Bibliotecas de Terceiros (em Ruby)

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', '__send__', '__id__'].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;
    
    for (i=0; i<RARRAY(ary)->len; i++) {
        if (rb_equal(RARRAY(ary)->ptr[i], item)) {
            return Qtrue;
        }
    }
    return Qfalse;
}

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 class `UmaClasse’ (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!

Advertisements
This entry was posted in Javascript, Ruby and tagged , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s