C# 4.0: casting dynamic to static
Asked Answered
L

2

12

This is an offshoot question that's related to another I asked here. I'm splitting it off because it's really a sub-question:

I'm having difficulties casting an object of type dynamic to another (known) static type.

I have an ironPython script that is doing this:

import clr
clr.AddReference("System")
from System import *

def GetBclUri():
    return Uri("http://google.com")

note that it's simply newing up a BCL System.Uri type and returning it. So I know the static type of the returned object.

now over in C# land, I'm newing up the script hosting stuff and calling this getter to return the Uri object:

dynamic uri = scriptEngine.GetBclUri();
System.Uri u = uri as System.Uri; // casts the dynamic to static fine

Works no problem. I now can use the strongly typed Uri object as if it was originally instantiated statically.

however....

Now I want to define my own C# class that will be newed up in dynamic-land just like I did with the Uri. My simple C# class:

namespace Entity
{
    public class TestPy // stupid simple test class of my own
    {
        public string DoSomething(string something)
        {
            return something;
        }
    }
}

Now in Python, new up an object of this type and return it:

sys.path.append(r'C:..path here...')
clr.AddReferenceToFile("entity.dll")
import Entity.TestPy

def GetTest():
    return Entity.TestPy(); // the C# class

then in C# call the getter:

dynamic test = scriptEngine.GetTest();
Entity.TestPy t = test  as Entity.TestPy; // t==null!!!

here, the cast does not work. Note that the 'test' object (dynamic) is valid--I can call the DoSomething()--it just won't cast to the known static type

string s = test.DoSomething("asdf"); // dynamic object works fine

so I'm perplexed. the BCL type System.Uri will cast from a dynamic type to the correct static one, but my own type won't. There's obviously something I'm not getting about this...

--

Update: I did a bunch of tests to make sure my assembly refs are all lining up correctly. I changed the referenced assembly ver number then looked at the dynamic objects GetType() info in C#--it is the correct version number, but it still will not cast back to the known static type.

I then created another class in my console app to check to see I would get the same result, which turned out positive: I can get a dynamic reference in C# to a static type instantiated in my Python script, but it will not cast back to the known static type correctly.

--

even more info:

Anton suggests below that the AppDomain assembly binding context is the likely culprit. After doing some tests I think it very likely is. . . but I can't figure out how to resolve it! I was unaware of assembly binding contexts so thanks to Anton I've become more educated on assembly resolution and the subtle bugs that crop up there.

So I watched the assembly resolution process by putting a handler on the event in C# prior to starting the script engine. That allowed me to see the python engine start up and the runtime start to resolve assemblies:

private static Type pType = null; // this will be the python type ref

// prior to script engine starting, start monitoring assembly resolution
AppDomain.CurrentDomain.AssemblyResolve 
            += new ResolveEventHandler(CurrentDomain_AssemblyResolve);

... and the handler sets the var pType to the Type that python is loading:

static void CurrentDomain_AssemblyLoad(object sender, AssemblyLoadEventArgs args)
{

    if (args.LoadedAssembly.FullName == 
        "Entity, Version=1.0.0.1, Culture=neutral, PublicKeyToken=null")
    {
        // when the script engine loads the entity assembly, get a reference
        // to that type so we can use it to cast to later.
        // This Type ref magically carries with it (invisibly as far as I can 
        // tell) the assembly binding context
        pType = args.LoadedAssembly.GetType("Entity.TestPy");
    }
}

So while the type used by python is the same on in C#, I'm thinking (as proposed by Anton) that the different binding contexts mean that to the runtime, the two types (the one in the 'load binding context' and the 'loadfrom binding context) are different--so you can't cast on to the other.

So now that I have hold of the Type (along with it's binding context) loaded by Python, lo and behold in C# I can cast the dynamic object to this static type and it works:

dynamic test = scriptEngine.GetTest();
var pythonBoundContextObject = 
       Convert.ChangeType(test, pType); // pType = python bound

string wow = pythonBoundContextObject .DoSomething("success");

But, sigh, this doesn't totally fix the problem, because the var pythonBoundContextObject while of the correct type, still carries the taint of the wrong assembly binding context. This means that I can't pass this to other parts of my code because we still have this bizzare type mismatch where the invisible specter of binding context stops me cold.

// class that takes type TestPy in the ctor... 
public class Foo
{
    TestPy tp;

    public Foo(TestPy t)
    {
        this.tp = t;
    }
}

// can't pass the pythonBoundContextObject (from above): wrong binding context
Foo f = new Foo(pythonBoundContextObject); // all aboard the fail boat

So the resolution is going to have to be on the Python side: getting the script to load in the right assembly binding context.

in Python, if I do this:

# in my python script
AppDomain.CurrentDomain.Load(
    "Entity, Version=1.0.0.1, Culture=neutral, PublicKeyToken=null");

the runtime can't resolve my type:

import Entity.TestPy #fails
Londalondon answered 6/5, 2010 at 23:41 Comment(5)
Pay attention to [AssemblyVersion]Bucko
It looks pretty much as if you were having different DLL versions of your satellite assembly being used by ironpython and C#.Bainbrudge
the dynamic object is of the correct type (confirmed via GetType()). I even changed the assembly ver number of the test assembly and re-ran it. looking again at the dynamicobjects type info, it shows the new assembly version number--but it still won't cast correctlyLondalondon
This may be a dumb question, but did you try upcasting to object before downcasting to the desired type?Pastel
Steven: doesn't work. I think that Anton is correct--the problem is probably that the assembly is loaded into the 'neither' context in Python which is not the same context as it is loaded into C# (where it would be in the load context). See Suzanne Cook's explanation. So the types don't line up because of the difference in context and thus the cast won't work.Londalondon
S
3

Here's an answer from the IronPython team which covers the same problem:

C# / IronPython Interop with shared C# Class Library

(Lifted from http://lists.ironpython.com/pipermail/users-ironpython.com/2010-September/013717.html )

Smoulder answered 16/9, 2010 at 7:25 Comment(1)
+1. Thanks a lot! doing a <code>engine.Runtime.LoadAssembly(typeof(WidgetEntities.Widget).Assembly);</code> works! The only prob I have with the solution is that to generalize the use (i.e. I want to be able to have a re-usable hosting component) means that I gotta know about all the possible references IronPython could in C# which basically means that I gotta load everything. Yuck.Londalondon
M
2

I bet that IronPython loads your entity.dll into a different assembly load context, so that you have two copies of it loaded and the types in them are of course different. You might be able to work around this issue by hooking AppDomain.AssemblyReslove/AppDomain.AssemblyLoad and returning your local assembly (typeof (Entity.TestPy).Assembly) when IronPython tries to load it, but I don't guarantee that this will work.

You don't experience this with System.Uri because mscorlib.dll (and maybe some other system assemblies) is treated specially by the runtime.

Update: IronPython FAQ states that if the assembly isn't already loaded clr.AddReferenceToFile uses Assembly.LoadFile, which loads into the 'Neither' context. Try accessing a method from Entity.TestPy before calling IronPython to load the assembly into the default Load context.

Maxine answered 7/5, 2010 at 17:12 Comment(3)
(1) I instantiated the Entiteis.TestPy class in C# prior to newing up the scriptEngine... no luck (2)The AssemblyResolve event only hits for 'System'... it never fires for 'Entity'. I think you are on the right track, but...Londalondon
attempting to do an Assembly.Load(...FQ assembly name...) in python. that line will execute, but when I attempt to then import Entities.Test it can't find it. It doesn't appear that Python is aware of the load binding contextLondalondon
Sorry, I meant hooking AppDomain.AssemblyLoad.Maxine

© 2022 - 2024 — McMap. All rights reserved.