12/08/2018, 17:57

Ruby hijacking

Bài viết tham khảo từ bài phát biểu của @tagamoris (Fluentd maintaine, Treasure Inc) và @joker1007 (CTO Repro) tại Ruby kaigi 2018. https://www.slideshare.net/tagomoris/hijacking-ruby-syntax-in-ruby Trong ruby core có tồn tại sẵn class Binding. Các object của class Binding đóng gói ngữ cảnh ...

Bài viết tham khảo từ bài phát biểu của @tagamoris (Fluentd maintaine, Treasure Inc) và @joker1007 (CTO Repro) tại Ruby kaigi 2018.

https://www.slideshare.net/tagomoris/hijacking-ruby-syntax-in-ruby

Trong ruby core có tồn tại sẵn class Binding. Các object của class Binding đóng gói ngữ cảnh (context) thực thi tại một số vị trí cụ thể trong mã code và giữ lại ngữ cảnh này để sử dụng trong tương lai. Các trạng thái của biến, method, block được giữ lại và có thể truy cập được. Binding object có thể được tạo ra bằng cách sử dụng Kernel#binding. (Xem ra cái này là nguồn gốc của binding.pry mà vẫn hãy dùng để debug khi xài gem pry).

Binding object có thể được truyền với tư cách là params thứ 2 khi gọi Kernel#eval để chỉ định ngữ cảnh mà params thứ nhất được đem ra tính toán trong phép toán eval.

Các method được support trong binding object có thể kể đến là

  • #eval
  • #local_variable_defined?
  • #local_variable_get
  • #local_variable_set
  • #local_variables
  • #receiver
class Demo
  def initialize(n)
    @secret = n
  end
  def get_binding
    binding
  end
end

k1 = Demo.new(99)
b1 = k1.get_binding(10)
k2 = Demo.new(-3)
b2 = k2.get_binding(-1)

b1.local_variable_get(:n) #=> 10
b1.eval("@secret")        #-> 99
b2.local_variable_get(:n) #=> -1
b2.eval("@secret")        #-> -3

Trong ví dụ này b1, b2 là các binding object được tạo ra khi call binding trong method get_binding. Từ binding object chúng ta có thể lấy được gía trị của local_variable n cũng như giá trị của biến instance @secret

Trick mà tác giả muốn đem ra sử dụng ở đây sẽ tập trung vào method local_variables trong binding object.

class Demo
  def binding_proxy(a, b, c)
    pp(here: :before, binding_id: binding.object_id, **dump(binding))
    yield binding
    pp(here:: after, binding_id: binding.object_id, **dump(binding))
  end
end
Demo.new.binding_proxy(10, "b", false) do |x|
  a = x.local_variable.get(:a)
  b = x.local_variable.get(:b)
  c = x.local_variable.get(:c)
  pp(here: :block1, binding_id: x.object_id, **.dump(x))
end
MBA~ tagomoriss ruby rubykaigi.2018.rb
{:here=>:before, :binding_id=>70179233373500, :a=>10, :b=>"b", :c=>false}
{:here=>:block1, :binding_id=>79179233183949, :a=>10, :b=>"b", :c=>false}
{:here=>:after, :binding_id=>70179241491049, :a=>10, :b=>"b", :c=>false}
MBA:~tagomoris$

Theo như kết quả ở đây thì mỗi lần binding ta thu được các giá trị biến môi trường giống nhau nhưng chỉ khác binding_id

Chuyện gì sẽ xẩy ra nếu can thiệp local_variable_set trong quá trình binding.

class Demo
  def binding_proxy(a, b, c)
    pp(here: :before, binding_id: binding.object_id, **dump(binding))
    yield binding
    pp(here:: after, binding_id: binding.object_id, **dump(binding))
    # d # => NameError
  end
end
Demo.new.binding_proxy(10, "b", false) do |x|
  x.local_variable_set(:d, 10)
  d = x.local_variable.get(:d)
  pp(here: :block1, binding_id: x.object_id, **.dump(x))
end
MBA~ tagomoris$ ruby rubykaigi.2018.rb
{:here=>:before, :binding_id=>70287664740260, :a=>10, :b=>"b", :c=>false}
{:here=>:block1, :binding id=>70287668531380, :a=>10, :b=>"b", :c=>false}
{:here=>:block1,
 :binding_id=>70287668531380,
 :d=>10,
 :a=>10,
 :b=>"b",
 :c=>false}
{:here=>:after, :binding_id=>78287664116940, :a=>10, :b=>"b", :c=>:false}
MBA:~ tagomoriss

Sau khi thêm 1 biến local d vào binding object thì biến d chỉ có tác dụng cho 1 binding instance duy nhất tại thời điểm biến local đó được set mà không ảnh hưởng tới các binding instance sau. (Có thể thấy tại log check after không có biến local d được thêm vào)

Tuy nhiên thay vì thêm 1 biến local mới nêú ta set lại giá trị cho 1 biến local cũ thì thế nào ?

class Demo
  def binding_proxy(a, b, c)
    pp(here: :before, binding_id: binding.object_id, **dump(binding))
    yield binding
    pp(here:: after, binding_id: binding.object_id, **dump(binding))
  end
end
Demo.new.binding_proxy(10, "b", false) do |x|
  x.local_variable_set(:d, 10)  
  x.local_variable_set(:a, 20)
  d = x.local_variable.get(:d)
  pp(here: :block1, binding_id: x.object_id, **.dump(x))
end
MBA: tagomoris$ ruby rubykaigi.2018.rb
{:here=>:before, :binding id->70162686885000, :a=>10, :b=>"b", :c=>false}
{:here=>:block,
 :binding_id=>70162686541660,
 :d=>20,
 :a=>20,
 :c=>false}
{:here->:after, :binding_id->70162698803900, :a=>20, :b=>"b", :c=>false)
MBA:~ tagomoriss

Khác với trường hợp thêm biến local như trước, khi set lại biến local cho binding object, giá trị của biến local sẽ bị overwrite !!

Kết luận

Binding#local_variable_set

  • giúp thêm biến vào chỉ trong 1 binding instance
  • nhưng overwrite giá trị của biến đã tồn tại trong ngữ cảnh gốc (original context)

Với class trong ruby bạn có thể định nghĩa lại các method hay thêm cá method, function tiện ích cho class có sẵn. Việc này gọi là "monkey patch". Tuy nhiên phạm vi thay đổi của nó là global nên tất cả mọi users đều nhìn thấy cùng 1 sự thay đổi. Điều này có thể gây ra 1 số side effect làm hỏng chương trình.

Do đó refiment được thiết kế để giảm impact của việc monkey patching của user khác đối với class bị monkey-patch. Nó cung cấp phương thức để có thể extend 1 class 1 cách local.

Ví dụ

class C
  def foo
    puts "C#foo"
  end
end

module M
  refine C do
    def foo
      puts "C#foo in M"
    end
  end
end

Đầu tiên class C được định nghĩa. Sau đó refinement của được tạo ra nhờ Module#refine. Module#refine tạo ra 1 anonymous module chứa sự thay đổi hay refinement của class. Chúng ta có thể activate refinement này bằng cách gọi như sau

using M
c = C.new
c.foo # prints "C#foo in M"

Tác giả joker1007 đã tạo ra 3 gem dưới đây nhằm phục vụ cho những mục đích riêng biệt

  • final (https://github.com/joker1007/finalist): cấm override method
  • override (https://github.com/joker1007/overrider): bắt buộc method phải có super method
  • abstract (https://github.com/joker1007/abstriker): bắt buộc override method

Finalist

Khi sử dụng gem này bạn chỉ cần thêm từ khoá final vào trước method được định nghĩa trong class thì các class kế thừa không thể override lại method đó

class A1
  extend Finalist

  final def foo
  end
end

class A2 < A1
  def foo # => raise
  end
end

Nếu 1 module chứa từ final method thì các class sử dụng module đó cũng không thể override lại method đó

module F1
  extend finalist

  final def foo
  end
end

class F2
  def foo
  end

  include F1 # => raise
end

Overrider

Gem này lấy ý tưởng của bên Java. Cú pháp override này đảm bảo là phải có super method thì mới cho phép override

class B1
end

class B2 < B1
  extend Overrider

  override def foo
  end
end # => raise false

Abstriker

Gem này cũng lấy ý tưởng từ Java. Các subclass kế thừa thì phải có implement cho abstract method. Trong trường hợp subclass không implement method đó thì sẽ raise lỗi Abstriker::NotImplementedError

class A1
  def foo
  end
end

class A2 < A1
  extend Overrider

  override def foo
  end
end

Hook methods

Vậy ý tưởng để thực hiện các method modifiers trên là gì ? Về bản chất tác giả sử dụng các hook methods, include, extend, + với các công cụ như TracePoint, Ripper

Các method hooks trong Ruby:

  • Module#method_added
  • Module#method_removed
  • Module#method_undefined
  • BasicObject#singleton_method_added
  • BasicObject#singleton_method_removed
  • BasicObject#singleton_method_undefined

Bằng cách sử dụng method hook, tác giả đã có thể implement các khai báo như protected, private Khi áp dụng method này vào viêc kiểm soát việc thừa kế, nó sẽ tạo ra những magic mà chúng ta không ngờ tới Trên thực tế implementation của gem finalist chẳng hạn còn phải tính đến những trường hợp như include module, extend, define_method nên chỉ áp dụng những method hook trên là vẫn chưa đủ nhưng không thể phủ nhận sự magical của những method hook trên.

.

0