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.
.