newproxy

Lua のユーザデータは、基本的に Lua を組み込む側で作るもので、純粋な Lua では使えません。
まぁ無くてもテーブルを使えばいいだけなのですが、テーブルだとオーバースペックだったり、
あるいは rawget/rawset によって破壊されてしまう恐れがあり、少し怖いです。


オーバースペックに感じる部分は、例えば 何物でもない値 - 野良C++erの雑記帳 とかがあり、
また rawget/rawset で破壊されると困るのは、読み取り専用のテーブルを作る場合など。
そういう場合に、ちょっとしたユーザデータが使えると、良いのではないでしょうか。


と思いつつ Luaソースコードを読んでいたところ、
どうやら newproxy という非公開関数を使うことで、実現できるようです。
こいつは引数を取らない、あるいは一つ取り、サイズ 0 のフルユーザデータを作る関数で、

  • 引数を渡さない(あるいは false を渡した)場合、メタテーブルの設定されていないユーザデータを作って返す
  • 引数として true を渡した場合、新しいテーブルをメタテーブルに設定したユーザデータを作って返す
  • 引数として newproxy 関数で引数を true として作られたユーザデータを渡された場合、そのユーザデータとメタテーブルを共有するユーザデータを新たにつくって返す

という処理をしている模様。


これを使えば、例えば前に挙げたタグを作る例では

do
  local newproxy = newproxy
  function createtag()
    return newproxy()
  end
end

local tag = createtag()

のように、読み取り専用の proxy を作る例では

do
  local newproxy = newproxy
  local getmetatable = getmetatable
  
  function readonly(t)
    local proxy = newproxy(true)
    
    local mt = getmetatable(proxy)
    mt.__index = t
    
    return proxy
  end
end

local t = readonly{ x = 1, y = 2, z = 3 }
print( t.x )  -- 1
t.x = "hoge"  -- error
t.u = "fuga"  -- error
rawset( t, "x", hoge )  -- error

と、かなり綺麗に書けます。


といっても非公開関数である以上、提供されているかどうかは不明ですし、次バージョンで削除されるかもしれません。
しかし、自分でビルドして使ってる場合などは、普通に便利に使えると思います。ない場合の代替も空テーブル使えばいいので楽ですし。
なお、メタテーブルを共有したプロキシを作る例は今回は特に挙げませんでしたが、気が向いたら。