f8g

Rubyで書いてJavaScriptで使う 2(managed codeを使いやすくする)

前回の続き。
http://d.hatena.ne.jp/arikui/20090428/1240866008
前は、

(なんちゃら).Invoke("呼ぶメソッドの名前", [引数])

という風でしたがやっぱり使いづらい。Ruby のオブジェクトをそのまま JavaScript にしたような感じで使いたい。ということで、多少の変更を加えます。

変更点

メソッドをLambdaで登録

http://d.hatena.ne.jp/arikui/20090305/1236259323
Lambda で登録すれば柔軟にオブジェクトが作れるしブラウザ側からメンバが見えます。

引数の渡し方を改善

ブラウザ - Silverlight 間の可変長引数の渡しが狂ってるなら、その辺は JavaScript に任せます。

new Function("return arguments.callee.Invoke(arguments)");

みたいなのを介して呼び出せば、Silverlight 側には常に ScriptObject が引数として渡されるようになります。実際に Managed Code を呼び出してるのは、この Function オブジェクトに登録されてる Invoke メソッドです。

Public Delegate Function F(Of T, TResult)(ByVal args As T) As TResult

Public Function ToScriptObject() As Browser.ScriptObject
    Dim obj As Browser.ScriptObject = window.CreateInstance("Object")
    Dim names As Collections.Generic.IList(Of String) = operations.GetMemberNames(result)
    
    For i = 0 To names.Count - 1
        Dim name As String = names(i)
        Dim fun As F(Of Browser.ScriptObject, Object) = Function(args As Browser.ScriptObject) Invoke(name, args)
        Dim method As Browser.ScriptObject = window.CreateInstance("Function", "return arguments.callee.Invoke(arguments)")
        method.SetProperty("Invoke", fun)
        obj.SetProperty(name, method)
    Next

    Return obj
End Function

デモ

http://arikui.github.com/run_ruby/TestPage.html
見えるぞ、私にもメソッドが見える。
http://gyazo.com/8bce1a2a268e8465bd1f3a1111e3eb0e.png

コード

Public Class ScriptableRuby
    Private Shared window As Browser.HtmlWindow = Browser.HtmlPage.Window
    Private Shared runtime As Microsoft.Scripting.Hosting.ScriptRuntime = IronRuby.Ruby.CreateRuntime
    Private Shared engine As Microsoft.Scripting.Hosting.ScriptEngine = runtime.GetEngine("IronRuby")
    Private Shared scope As Microsoft.Scripting.Hosting.ScriptScope = engine.CreateScope
    Private Shared operations As Microsoft.Scripting.Hosting.ObjectOperations = engine.CreateOperations(scope)

    Public Shared Function Create(ByVal source As String) As RubyObject
        Dim result = engine.Execute(source, scope)
        Return New RubyObject(ScriptableRuby.operations, result)
    End Function

    Public Shared Function Arrayify(ByVal args As Browser.ScriptObject, Optional ByVal arity As Integer = -1) As Object()
        Dim length As Integer = args.GetProperty("length")
        arity = IIf(arity < 0, length, arity)
        Dim _args(arity - 1) As Object

        For i As Integer = 0 To arity - 1
            _args(i) = args.GetProperty(i)
        Next

        Return _args
    End Function

    <Browser.ScriptableType()> _
    Public Class RubyObject
        Private result As Object
        Public Delegate Function F(Of T, TResult)(ByVal args As T) As TResult
        Public Delegate Function F(Of TResult)() As TResult
        
        Public Sub New(ByVal operations As Microsoft.Scripting.Hosting.ObjectOperations, ByVal result As Object)
            Me.result = result
        End Sub

        Public Function ToScriptObject() As Browser.ScriptObject
            Dim obj As Browser.ScriptObject = window.CreateInstance("Object")
            Dim names As Collections.Generic.IList(Of String) = operations.GetMemberNames(result)
            
            For i = 0 To names.Count - 1
                Dim name As String = names(i)
                Dim fun As F(Of Browser.ScriptObject, Object) = Function(args As Browser.ScriptObject) Invoke(name, args)
                Dim method As Browser.ScriptObject = window.CreateInstance("Function", "return arguments.callee.Invoke(arguments)")
                method.SetProperty("Invoke", fun)
                obj.SetProperty(name, method)
            Next

            Return obj
        End Function

        <Browser.ScriptableMember()> _
        Public Function Invoke(ByVal name As Object, Optional ByVal args As Browser.ScriptObject = Nothing) As Object
            Dim member As IronRuby.Builtins.RubyMethod = operations.GetMember(result, name)

            If args Is Nothing Then
                Return engine.Operations.Invoke(member)
            Else
                Return engine.Operations.Invoke(member, ScriptableRuby.Arrayify(args, member.Info.GetArity()))
            End If
        End Function

        <Browser.ScriptableMember()> _
        Public Function Invoke(ByVal name As Object, ByVal ParamArray args As Object()) As Object
            Return engine.Operations.Invoke(operations.GetMember(result, name), args)
        End Function
    End Class
End Class