Accessing IronPython objects from native JScript (using IReflect)
The DLR aims to enable dynamic languages like IronPython and IronRuby to access and minipulate objects created by each other. But what about dynamic languages like JScript implemented using unmanaged code? It is sometimes useful for IronPython and native JScript to interact with each other. Consider the following scenarios:
- IronPython could be used to host a System.Windows.Forms.WebBrowser control which loads a Web page with native JScript. See this MSDN article for a description of the WebBrowser control. Srivatsn discusses some of the issues related to using it from IronPython here.
- Native Web browser JScript could talk with IronPython running in a Silverlight 2 application embedded in the Web page. (Note that unfortunately, this scenario will not currently work. See "Issues and Limitations" #1 below.)
- VBA macros running inside Office could load and run IronPython scripts.
IronPython is now able to access native JScript objects because of the IDispatch support we have added to the DLR.
For the reverse direction of native JScript accessing IronPython objects, the IronPython objects need to be marshalled to unmanaged code as IDispatch (and ideally also IDispatchEx). I had previously discussed how managed types can implement System.Reflection.IReflect to be marshalled as IDispatch. Reading Srivatsn's blog, I decided to see how I could build on this IReflect <-> IDispatch mapping. This turns out to be fairly straightforward. I have implemented a Python modules called IReflectUtils. This allows an IronPython class to trivally indicate that it wants to make itself accessible to native JScript. The IronPython class declaration looks like the following:
from IReflectUtils import IReflectBase
print "In TestClass.foo"
All it takes is inheriting from a single class IReflectBase. No other change is needed. At that point, the Python class becomes accessible from native JScript. JScript can invoke any of the methods of the Python class, including dynamically added methods. JScript can also read and write any of the attributes defined on the object. Tada!
Note that because of a bug in IronPython, IReflectBase needs to be the first base type specified. So the class declaration in Srivatsn's blog would become:
class MyForm(IReflectBase, Form):
How it works
Under the hoods, IReflectBase implements IReflect. It needs to implement the following two main functions:
- IReflect.GetMethods - this is implemented by calling "dir" on the object and returning all the attributes as method names. The function needs to return the method names as an array of MethodInfo, and so we have to create a subtype of MethodInfo which basically just knows the name it corresponds to.
- IReflect.InvokeMember - this turns into a "getattr(target, name)", possibly followed by a call to the returned object. The invokeAttr parameter that is passed into the method indicates whether the Python attribute should just be returned (if BindingFlags.GetProperty is set), or if it should be invoked (if BindingFlags.InvokeMethod is set)
Issues and limitations
- This solution will not work on Silverlight 2 which currently does not include any COM Support. Hence, it does not support the IReflect <-> IDispatch mapping. It would be possible to port this functionality from .NET Frameworks to a future version of Silverlight.
- This solution will only work with IE, since only Microsoft's JScript implementation is based on IDispatch. Firefox's implementation uses a similar but different interface that is part of XPCOM. I do not what the XPCOM interface looks like, but if it is similar to IDispatch, it should be possible to map IReflect to it as well.
- Python classes with __getattribute__ will not work. The CLR calls IReflect.GetMethodNames to get a full list of methods supported by the object. This is different than IDispatch.GetIDsOfNames which only supports querying whether a single given method is supported or not (which is similar to how __getattribute__ works). Because of this, such Python classes will not be able to expose all the method names that __getattribute__ recognizes. IReflect does have a GetMethod method that could have been used to query whether a specific method existed, but the IReflect <-> IDispatch mapping does not use this. A theoretical solution is for IReflect.GetMethodNames to return the complete infinite set of all character combinations, but this does not really work. Ruby's method_missing will run into the same issue. This will need a creative fix to the IReflect <-> IDispatch mapping, or for a new interface to be added to the CLR which more closely follows the IDispatch model.
- This feature could be shipped in different ways. The attached sample show a simple Python module. This could even be used with IronPython 1.X. However, it requires that all Python classes explicitly opt in, which may not work well for large existing code bases. Also, builtin types with dynamic behavior implemented by IronPython.dll will not work as the user cannot make them implement IReflect. The feature could be built into the IronPython runtime so that all IronPython user-defined classes would implicitly inherit from IReflect, and any builtin types could be made to implement IReflect.
- The current solution relies on inheriting from IReflect (via IReflectBase). This can lead to name collisions if the user defines method with names which are the same as some significant IReflect method.
- VBScript does not require the use of paranthesis while calling a method with no arguments. This makes ambiguous the syntax for an instance method call and a property access. This can cause some issues since the IReflect.InvokeMember needs to know whether a property should be accessed or if the attribute should be invoked. Similarly, VBScript and IDispatch are case-insensitive whereas Python is case-sensitive. The IReflect <-> IDispatch mapping seems to do a case-insensitive search and randomly picking an attribute if there are multiple matches.
- We could support richer interaction between managed and unmanaged dynamic languages by taking advantage of the System.Runtime.InteropServices.Expando.IExpando <-> IDispatchEx mapping that the CLR supports. This would allow addition and deletion of attributes. This is currently left as an exercise for the user. Also, note that this feature has bugs in it, and the support may not be sufficient for all the scenarios you would care about.
About the attached file
IReflectUtils.zip contains the following files:
- IReflectUtils.py - The main Python module file. This file can be used in your own IronPython projects.
- TestClass.py - A sample Python class that inherits from IReflectUtils.py
- ManagedFactory.cs - A bridge between test.js/test.vbs and TestClass.py. It includes a class that is ComVisible and hence can be instantiated by test.js and test.vbs. The class loads TestClass.py and passes the IronPython object back to test.js and test.vbs. See the top of the file for instructions on compiling and registering the file.
- test.js and test.vbs - The driver files which instantiate ManagedFactory, use it to get an IronPython object, and then access the object. See the top of the file for instructions on running the files.