M
MatthewRoberts
Howdy All,
I am having difficulty with two-way communication across AppDomains in
an attempt to dynamically script applications. Everything works as
expected, except when using ByRef parameters.
The below explanation is lengthy, but well worth the read. If you can
help me, I'd gladly share this code which has greatly helped my
development of extensible applications.
I have developed an easy to use scripting engine for .NET that handles
loading and unloading of a secondary AppDomain automatically. I call
the object "SafeScriptingEngine", and it has properties for the source
language, source code, and any needed references and imports. When its
compile method is called, the code is compiled by the appropriate
compiler, then it creates a new AppDomain, loads the compiled code into
the AppDomain, and uses a Class Factory approach to return a well-known
interface.
This simple interface contains two functions called "MethodExists",
which accepts a function name and a list of parameters then return a
Boolean of whether the function exists, and "CallMethod", which again
accepts a function name and a list of parameters then actually calls
the function.
Public Interface IRemoteLoader
Function Invoke(ByVal methodName As String, ByVal parameters() As
Object) As Object
Function MethodExists(ByVal methodName As String, ByVal parameters()
As Object) As Boolean
End Interface
So, an example of using this engine looks like:
Dim sse As New SafeScriptingEngine()
sse.Language = Languages.VisualBasic
sse.SourceCode = "" & _
"Public Function UpperString(ByVal s As String) As String" & _
" Return s.ToUpper" & _
"End Function"
If sse.Compile() Then
If sse.MethodExists("UpperString", New Object() { "" }) Then
MsgBox(sse.CallMethod("UpperString", New Object() { "this
string will be converted to uppercase" }))
End If
End If
All of this works great. There are no memory leaks, and all compilers,
AppDomains, and assemblies are unloaded as desired and expected. In
fact, I designed the engine to be inheritable such that functions can
be created that are strongly typed. As you can see, the default engine
expects a non-strongly-typed array of Objects.
To prove the usability of the engine, I created a "SimpleStringScript"
which inherits from the "SafeScriptEngine". The "SimpleStringScript"
accepts only the part of code that does the manipulation, such as
"s.ToUpper" in the example above. In the background, it adds function,
parameters, and Return statement. Also, with the "SimpleStringScript",
there is no need for "MethodExists" that accepts parameters. Instead,
it has a "StringMethodExists" function that accepts no parameters,
which then calls "MethodExists" on the base "SimpleStringScript" with a
String Object as the parameter. Likewise, there is no need to call
"CallMethod" with a function name and Object array. Instead, it has a
"CallStringMethod" function that take a String as a parameter, which in
turn calls the base "CallMethod" with the appropriate function name and
the String parameter encased in an Object array.
Public Class SimpleStringScript
Inherits SafeScriptingEngine
Public Function CallStringScript(ByVal s As String) As String
Public Function StringMethodExists() As Boolean
...
End Class
Again, all of this works great, just as desired and expected.
The problem arises when you create a script that uses ByRef parameters.
I would like a function that looks like:
Public Sub MyCustomFunction(ByRef e as MyEventArgs)
e.MyObject.MyProperty = "some value"
End Sub
I quickly discovered that any parameter being passed into another
AppDomain must be Serializable, as do any objects that are referenced
in its hierarchy. Otherwise, it will throw a SerializationException
when you try to pass in the parameter. So, I changed all objects that
are referenced in the EventArgs to be Serializable, and that solved the
problem when calling. However, upon return to the calling AppDomain,
the objects and values in "e" are still the same as before the call.
None of the manipulations taking place in the script from the secondary
AppDomain are carried back to the calling AppDomain.
I figure this is because, as expected with the Serializable objects,
the "e" is being serialized, sent across the AppDomain boundary, then
deserialized. When it is deserialized, an entirely new object is
created. Changes made are not sent back across the boundary simply by
using ByRef arguments. Unfortunately, I'm not sure exactly how to solve
this problem.
I think I should be sending an Interface as my argument, rather than an
object of MyEventArgs. As long as the Interface is well-known in both
AppDomains, I think the scripting AppDomain should know what to call,
and when it does, the call should be marshaled back to the primary
AppDomain via the Interface. Basically, the Interface is just a pointer
back to the actual object. Can anyone confirm this to be True or False?
If False, please HELP!
If True, then is it possible to simply wrap the MyEventArgs object into
an Interface, such as "IMyEventArgs", then change the scripted function
to accept the Interface rather than the object, then pass an Interface
rather than an object into the method? Would that suffice, so long as
both AppDomains are aware of the Interface and anything it references?
Obviously, if the Interface referenced a type that is not loaded in the
secondary AppDomain, then there would be problems because the script
would not know how to use it.
Or, do I have to wrap the EventArgs into an Interface, as well as wrap
every object that it references into an Interface. Meaning, again using
the example above, would I also need to create an "IMyObject"
Interface?
My workaround for the time being is to return the object being
manipulated as the return value, that way, the new object (created from
deserialization) is again serialized upon return. However, this
requires the scripter to know that they need to return it. Using C#,
that is no big deal because the compiler will tell you when there is
nothing returned. However, in Visual Basic, if you fail to supply a
return value, you simply get Nothing. But, since I will be writing the
scripts, I'll deal with the workaround for now. But, I sure would like
ByRef to work across AppDomains.
Any suggestions?
Thanks in advance,
Matthew
___________________________________
Matthew Roberts
Framework Architect
Business Process Solutions
SOURCECORP
Dallas, TX 75204
matthewroberts (at) srcp (dot) com
I am having difficulty with two-way communication across AppDomains in
an attempt to dynamically script applications. Everything works as
expected, except when using ByRef parameters.
The below explanation is lengthy, but well worth the read. If you can
help me, I'd gladly share this code which has greatly helped my
development of extensible applications.
I have developed an easy to use scripting engine for .NET that handles
loading and unloading of a secondary AppDomain automatically. I call
the object "SafeScriptingEngine", and it has properties for the source
language, source code, and any needed references and imports. When its
compile method is called, the code is compiled by the appropriate
compiler, then it creates a new AppDomain, loads the compiled code into
the AppDomain, and uses a Class Factory approach to return a well-known
interface.
This simple interface contains two functions called "MethodExists",
which accepts a function name and a list of parameters then return a
Boolean of whether the function exists, and "CallMethod", which again
accepts a function name and a list of parameters then actually calls
the function.
Public Interface IRemoteLoader
Function Invoke(ByVal methodName As String, ByVal parameters() As
Object) As Object
Function MethodExists(ByVal methodName As String, ByVal parameters()
As Object) As Boolean
End Interface
So, an example of using this engine looks like:
Dim sse As New SafeScriptingEngine()
sse.Language = Languages.VisualBasic
sse.SourceCode = "" & _
"Public Function UpperString(ByVal s As String) As String" & _
" Return s.ToUpper" & _
"End Function"
If sse.Compile() Then
If sse.MethodExists("UpperString", New Object() { "" }) Then
MsgBox(sse.CallMethod("UpperString", New Object() { "this
string will be converted to uppercase" }))
End If
End If
All of this works great. There are no memory leaks, and all compilers,
AppDomains, and assemblies are unloaded as desired and expected. In
fact, I designed the engine to be inheritable such that functions can
be created that are strongly typed. As you can see, the default engine
expects a non-strongly-typed array of Objects.
To prove the usability of the engine, I created a "SimpleStringScript"
which inherits from the "SafeScriptEngine". The "SimpleStringScript"
accepts only the part of code that does the manipulation, such as
"s.ToUpper" in the example above. In the background, it adds function,
parameters, and Return statement. Also, with the "SimpleStringScript",
there is no need for "MethodExists" that accepts parameters. Instead,
it has a "StringMethodExists" function that accepts no parameters,
which then calls "MethodExists" on the base "SimpleStringScript" with a
String Object as the parameter. Likewise, there is no need to call
"CallMethod" with a function name and Object array. Instead, it has a
"CallStringMethod" function that take a String as a parameter, which in
turn calls the base "CallMethod" with the appropriate function name and
the String parameter encased in an Object array.
Public Class SimpleStringScript
Inherits SafeScriptingEngine
Public Function CallStringScript(ByVal s As String) As String
Public Function StringMethodExists() As Boolean
...
End Class
Again, all of this works great, just as desired and expected.
The problem arises when you create a script that uses ByRef parameters.
I would like a function that looks like:
Public Sub MyCustomFunction(ByRef e as MyEventArgs)
e.MyObject.MyProperty = "some value"
End Sub
I quickly discovered that any parameter being passed into another
AppDomain must be Serializable, as do any objects that are referenced
in its hierarchy. Otherwise, it will throw a SerializationException
when you try to pass in the parameter. So, I changed all objects that
are referenced in the EventArgs to be Serializable, and that solved the
problem when calling. However, upon return to the calling AppDomain,
the objects and values in "e" are still the same as before the call.
None of the manipulations taking place in the script from the secondary
AppDomain are carried back to the calling AppDomain.
I figure this is because, as expected with the Serializable objects,
the "e" is being serialized, sent across the AppDomain boundary, then
deserialized. When it is deserialized, an entirely new object is
created. Changes made are not sent back across the boundary simply by
using ByRef arguments. Unfortunately, I'm not sure exactly how to solve
this problem.
I think I should be sending an Interface as my argument, rather than an
object of MyEventArgs. As long as the Interface is well-known in both
AppDomains, I think the scripting AppDomain should know what to call,
and when it does, the call should be marshaled back to the primary
AppDomain via the Interface. Basically, the Interface is just a pointer
back to the actual object. Can anyone confirm this to be True or False?
If False, please HELP!
If True, then is it possible to simply wrap the MyEventArgs object into
an Interface, such as "IMyEventArgs", then change the scripted function
to accept the Interface rather than the object, then pass an Interface
rather than an object into the method? Would that suffice, so long as
both AppDomains are aware of the Interface and anything it references?
Obviously, if the Interface referenced a type that is not loaded in the
secondary AppDomain, then there would be problems because the script
would not know how to use it.
Or, do I have to wrap the EventArgs into an Interface, as well as wrap
every object that it references into an Interface. Meaning, again using
the example above, would I also need to create an "IMyObject"
Interface?
My workaround for the time being is to return the object being
manipulated as the return value, that way, the new object (created from
deserialization) is again serialized upon return. However, this
requires the scripter to know that they need to return it. Using C#,
that is no big deal because the compiler will tell you when there is
nothing returned. However, in Visual Basic, if you fail to supply a
return value, you simply get Nothing. But, since I will be writing the
scripts, I'll deal with the workaround for now. But, I sure would like
ByRef to work across AppDomains.
Any suggestions?
Thanks in advance,
Matthew
___________________________________
Matthew Roberts
Framework Architect
Business Process Solutions
SOURCECORP
Dallas, TX 75204
matthewroberts (at) srcp (dot) com