在 Kobako:執行腳本已經看過一個完整的運作是怎樣設計的,但是現有的設計還有一些不方便的地方,當我們想將一個 Ruby 的物件在沙盒(Sandbox)中呼叫時,卻很難直接使用到那些回傳值。
綁定的限制
舉例來說,我們希望讓 Agent(代理)使用 Code Mode 來直接對 Rails 的資料庫查詢,可能會像這樣實作。
1s = Sandbox.new
2s.bind("User", User)
3
4# Agent's tool
5s.eval("User.find_by(email: 'user@example.com')")
6# => ?我們會發現,這個設計無法被實作,因為 User.find_by 會回傳一個 User 物件,而沙盒內的 mruby 不知道 User 是什麼,因此無法建立,也就無法回傳。
這是因為 MessagePack 只支援基本的數值類型,如果要把每一種物件都覆蓋幾乎不可能,作為替代通常會嘗試 #to_msgpack 看看是否有定義如何轉換成 Hash、Number、String 這種基本的數值,但我們在宿主(Host)和客體(Guest)之間就會花費大量力氣在轉換資料。
而且很可能不必要,因為 User 物件是傳進去再傳出來,這樣是很浪費的。
指標機制
要解決這個問題,已經有現成的機制可以參考,那就是「指標」的概念,在軟體中許多機制就是靠「指向記憶體某個位置」來運作的,這剛好很適合我們的需要。
在 Kobako 的設計,當我們要傳入客體給 mruby 使用時,會先檢查是不是能轉換的物件,如果不能轉換就會向 Kobako 申請一個編號,把傳入的物件轉為 Kobako::Handle 物件。
1def deep_wrap(value, handle_table)
2 case value
3 when ::Array then value.map { |element| Utils.deep_wrap(element, handle_table) }
4 when ::Hash then value.transform_values { |val| Utils.deep_wrap(val, handle_table) }
5 else
6 wire_representable?(value) ? value : Kobako::Handle.new(handle_table.alloc(value))
7 end
8end因為物件也可能躲在 Array 或者 Hash 中,因此會做一個遞迴的檢查,把所有物件確認清楚後包裝成一個可以被 MessagePack 完整解析的物件。
這裡借用了 MessagePack 的「擴充(Extension)」功能,Kobako 會告知 MessagePack 遇到 Kobako::Handle 物件時,只需要傳遞編號即可。
如此一來,原本的 User 物件,就會變成一個 #<Handle id=1> 被傳入,當回傳值看到 #<Handle id=1> 時,只要再詢問 Kobako 編號 1 的物件是什麼,我們就能拿回 User 這個物件。
容易溝通
這樣的設計,是透過 Kobako 維護兩張表格達成的,透過這兩張表格不論是在宿主還是客體,都能很簡單的進行溝通跟互動。
綁定(Binding)用於把已知的物件提供到客體裡面,可以使用宿主的功能。
| Binding | Class |
|---|---|
"User" | User |
"Workspace" | Workspace |
句柄(Handle)機制則提供回傳數值,可以不受限於已知的綁定,而且是在不透明的前提下提供。
| Handle | Object |
|---|---|
| 1 | #<User id=1> |
| 2 | #<Workspace id=5> |
不論是綁定還是句柄,都具備了 Kobako 設計的 #method_missing 的機制,因此都能夠傳回給宿主,呼叫宿主的能力,也因此組合起來後,沙盒內能做的事情就被很大的拓展開來。
1s = Sandbox.new
2s.bind("User", User)
3
4s.eval("User.find_by(email: 'user@example.com')")
5# => #<User id=1>
6
7s.eval("User.where(role: :admin).pluck(:id)")
8# => [1, 5, 9]就算是 ActiveRecord 的 Method Chaining(方法鏈)也能夠很輕鬆的使用,不過這也變成 Kobako 的一大挑戰,等我們把大多數功能釐清後,再回來看這個機制,就會發現越好用的機制同時也越危險。
喜歡這篇文章?請我喝杯奶茶 🧋