Fork me on GitHub
#clr
<
2023-03-14
>
elena.poot19:03:33

Interop question: I'm trying to call a generic method with the signature Entity.Get<T>(IField field) field is a class type, T may or may not be (not, in my current case). My attempt is: (.Get entity (type-args Int32) field) The exception thrown says: The type arguments for method 'Get' cannot be inferred from the usage. Try specifying the type arguments explicitly. I've verified that entity and field are objects of the correct type. I don't see what I'm doing wrong here. The example in the documentation is nearly identical. The only oddity is that the type argument applies only to the return value, and none of the calling arguments. I'm using the alpha3 release. (I've also tried adding a type hint for the 'field' argument, to no avail.)

dmiller17:03:36

If you can share a few more details, I can try to figure it out. interop with generics is tricky. SOmething I hope to solve more systematically in the next iteration.

elena.poot02:03:24

The environment is complex, but that particular call isn't. It just doesn't make sense in most cases I guess. I'm building a plugin. My cljr code is called from me C# plugin, and calls back into the API of the host program. (And as it happens, that API is mostly an interop layer over the C++ core application.) So in the call I mentioned, Entity is in effect a user-defined data structure. It is defined by a schema that declares what fields it contains, and their types. The Field object in the call is one of those field definition objects, so this call is attempting to get a value from that user-defined object, by specifying the field object from the schema that tells it the piece of data I want. And since the type of that data is defined by the field and can't be inferred in any way by the code, the method is generic, with the type parameter being the return value. I think you COULD write such a thing in pure C#, but this just proxies into C++, so they check the type parameter for each of the potential data types of a field and call into their proxy layer accordingly. So in my case, the schema defines a single field, of type integer. Thus I retrieve the Entity object, specify the Field object that defines that field, and specify Int32 as the type parameter to the method: int i = Entity.Get<Int32>(field); in C# or (.Get entity (type-args Int32) field) Where entity and field are C# objects of class type previously retrieved via other API calls. I'm not sure what else I can tell you about it, but I'll be happy to answer any questions. (BTW, This isn't urgent for me, I've worked around it by writing a non-generic C# method I can call that makes that call for me. Conveniently enough, we have a wrapper library around that API to smooth over version differences, since we support multiple versions of the host application, so adding methods is easy in this case.)

dmiller02:03:31

No other overloads on Get?

elena.poot03:03:09

I will confirm in just a bit and get back to you, just because it is such a generic name, but I am pretty sure there are not.

dmiller03:03:56

The type-args should apply to the types mentioned in the generic definition of the method, so I'm not sure the fact that its the return type being a type variable is an issue. i'll see if I can mock up something with this behavior.

👍 2
elena.poot03:03:36

I, I was wrong, there are 2 overloads: public T Get<T>(IField field) public T1 Get<T1>(string fieldName)

dmiller03:03:18

That should be enough to get me started. I'll take a look tomorrow.

dmiller04:03:03

I'm curious as how Get gets to be generic on the return type only.

elena.poot04:03:29

Yeah, I've never seen that before except in this one case. I've used that call from C# and it didn't click at the time that it was odd, but it is, and I've never seen it done like this anywhere else. But it does work fine in C#. There just aren't many cases where your return type wouldn't depend on the inputs. Of course, in this case, the input is an object containing the definition (basically where to get the field and what type it is) and that's all being done in the underlying C++ code. But I could see doing something like this with database retrieval, for instance. Of course, boxing is a thing and most people would just return Object. 🙂

dmiller19:03:17

I'm guessing you didn't go far enough with the type hints. The inference mechanism around generic types is not robust. Here is my test code. On the C# side, an interface IField implemented by a class Field and a class TestClass that has two overloads for a method Get with a type parameter for the return type.

namespace GenericMethodTest
{
    public interface IField
    {
        int M();
    }

    public class Field : IField
    {
        public int M()
        {
            return 42;
        }
    }

    public class TestClass
    {
        public T Get<T>(IField field) 
        {
            return (T)Convert.ChangeType(field.M(), typeof(T)); 
        }

        public T Get<T>(string s)
        {
            return (T)Convert.ChangeType(s.Length, typeof(T));
        }
    }
}
Here is my ClojureCLR transcript:
user=> (assembly-load-from "GenericMethodTest")
#object[RuntimeAssembly 0x153bc03 "GenericMethodTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"]
user=> (import '[GenericMethodTest Field TestClass])
GenericMethodTest.TestClass
user=>
user=>
user=> (def f (Field.))
#'user/f
user=> (.M f)
42
user=>
user=> (def tc (TestClass.))
#'user/tc
user=>
user=> (.Get ^TestClass tc (type-args Single) ^Field f)
42.0
user=> (.Get ^TestClass tc (type-args Single) "abc")
3.0
We need all three type hints. Really, we should only need one, but the runtime mechanism apparently is not good enough. That uses the dynamic call-site mechanism and apparently the discriminator used at runtime has a deficiency. The required type is the type-args -- that would be necessary in any case because there is no way to infer the return type from any other information. It is not related to Get, TestClass or the argument type. The other two type hints means that reflection is not necessary at evaluation time -- it can figure out the exact method to call with those type hints. The TestClass hint tells us what class to look up the Get method on. The type hint of Field on f is required because of the overload on the Get. Again, ideally, the runtime reflection should be able to figure this out. So that I can work on. But if you can type hint fully at the call-site, it should work. (No type hint needed on the string -- it is a constant and carries its type without our help.) Unless your situation has some wrinkle that mine doesn't. I'll look into the runtime reflection mechanism on the dynamic call-site. It is a place I fear to tread.

elena.poot19:03:45

Thank you, I'll give that a try today!

elena.poot22:03:50

Yep, you're exactly right, specifying both type hints and the type-args makes it work. Thanks!

dmiller01:03:14

cool. I think I know how to fix the reflection mechanism so one could get by with only the type-arg hint to get the return type. The problem I'm having in implementing it is in a silly little shim for which I have to generate the explicit IL. And I'm messing it up somehow. Sigh.

elena.poot01:03:19

Oh that sounds nasty. Best of luck with it!

dmiller01:03:36

Thanks. It's like five lines of IL gen. But I have to add an array creation. And I keep messing it up. When I redo this, I hope to develop some better tools to help debug IL gen.

👍 2
dmiller02:03:31

Nailed it.

user=> (def tc (TestClass.))
#'user/tc
user=> (def f (Field.))
#'user/f
user=> (.Get tc (type-args Single) f)
42.0
user=> (.Get tc (type-args Single) "abc")
3.0
As I mentioned, you can't get rid of the type-args because there is no way to infer the return type. The type-args mechanism was added fairly late. The example in the wiki entry you read was actually the cause. I did not quite get the fully dynamic case right, the case where reflection is done at run time (in the form of a dynamic callsite). (I actually thought the calls to the underlying Microsoft code could handle it and I didn't have an example where it failed.) I have to check two other places involving similar callsite code, but I'm pretty sure generic methods and type args do not come into play there, so this fix is probably done. I'll put it in the next alpha release. As I mentioned above, this code is a place I fear to tread. I'm very happy the fix popped out pretty quickly. Thanks for bringing this to my attention.

elena.poot02:03:57

Awesome, congrats. Definitely an edge case, and I'm glad it wasn't too difficult.