HaxeFoundation / haxe

Haxe - The Cross-Platform Toolkit
https://haxe.org
6.12k stars 650 forks source link

[cs] Wrong generic type definition #5434

Closed Neverbirth closed 5 years ago

Neverbirth commented 8 years ago

Having for example the following piece of code:

interface ITest<K, V>
{
    function keys():Array<K>;
    function values():Array<V>;
}

interface ISubTest extends ITest<String, String>
{
}

Generates the following C# code for ISubTest:

public interface ISubTest : global::haxe.lang.IHxObject, global::ITest<object, object> {

}

As you can see it completely loses the types we want to use. I tried different types, and I was always getting object for the generic type. Furthermore, this one:

interface ITest<K, V>
{
    function keys():Array<K>;
    function values():Array<V>;
}

interface ISubTest extends ITest<String, Dynamic>
{
}

class TestClass implements ISubTest
{
    public function keys():Array<String>
    {
        return null;
    }

    public function values():Array<Dynamic>
    {
        return null;
    }
}

Doesn't compile at all, because it generates some invalid implementation of the interface in TestClass:

    global::Array global::ITest<object, object>.keys() {
        return ((global::Array) (this.keys()) );
    }

    public virtual global::Array<object> keys() {
        return null;
    }

    public virtual global::Array values() {
        return null;
    }

I would say this is a rather important issue.

nadako commented 8 years ago

The second one is a real issue.

As for the object - that's how the c# target currently works. As @waneck explained before, since the memory layout is the same, there's no real benefit of working around the haxe/c# type system differences to make the code look more "native", and by some tests it even worked faster. I'd like to review/check that again though, because this topic is brought quite often.

Neverbirth commented 8 years ago

? Would love to see those tests. While what you say is true for some cases, it's not for all of them. Think for example of a generic that in the end is creating an array for ints, with the code used for c# you'll end creating an array of objects, in that case the memory layout is not the same and haxe code will be much slower for some uses. Also, what if you use constraints for the generic type? I didn't check this use case, but do you rely on reflection to access known methods and properties? in that case it's clear it will also be slower.

There are other cases too, but I'd say that in a lot of cases the produced code will be slower (admittedly in a lot of them the difference will be minimal), maybe those tests were not taking into account GC garbage, starting up overload and other things that can take an effect into the final results?

EDIT: Tested it, I see that constraints are not converted over to C# code, and that in fact reflection is used unless we define the exact type to be used in the interface or class declaration.

nadako commented 8 years ago

Well, for basic types (int,double,bool) it generates proper type, it's just all others that are pointers anyway, but yeah, we should look into it at some point in future.

Neverbirth commented 8 years ago

I see, I thought the object replacement was used everywhere, my bad. There may be some cases where it can still be a problem, like arrays of primitive types, but I think this one would be very uncommon.

At any rate, that bug when using Dynamic is way more important, it's for example making hexMachina not usable when targetting C#, a real shame as it's already working for a few targets.

vroad commented 8 years ago

Is there any way to avoid this issue? Haxe falls back to reflection when iterating through Map values like this.

https://github.com/openfl/starling/blob/8d3f19b9594870db8f406442ba5cff476395393a/starling/rendering/BatchProcessor.hx#L211

vroad commented 7 years ago

I'm not sure why losing generic parameter makes program faster either. That will cause lots of casts, and makes code size bigger. Compilers cannot generate optimized code from such big code.

vroad commented 7 years ago

Even when reflection is not used it could be bad for performance because of lots of casts.

Also, losing type information is bad if you want to make a library for existing .NET application, or if you want to use that library from Haxe with -net-lib. You need to do cast everywhere to get an instance of correct type from arrays/maps.

Neverbirth commented 7 years ago

Also, losing type information is bad if you want to make a library for existing .NET application, or if you want to use that library from Haxe with -net-lib. You need to do cast everywhere to get an instance of correct type from arrays/maps.

That's a good point... I didn't try it, but what if you have a library that requires to get for example some GenericClass<CustomClass> instance? AFAIK GenericClass<Object> won't work in the cases where variance is not defined, will it?

vroad commented 7 years ago

You mean we should support casting to GenericClass<object> from GenericClass<CustomClass> in some way? Then, should all generic classes have wrapper object like arrays/maps to support variance? That may work, but makes generated code complex, and creates many objects for casting.

Could that be the other way around? Just create class instances with type parameter(SomeList<CustomClass>), and automatically upcast to CustomClass if you want to push some element, and downcast to `object' when you get some element from object. This way we don't need wrappers, but list cannot contain any kind of object anymore.

Should a List be able to contain any kind of object? Then it's not possible to support variance in this way. but you can't insert objects to Array<Int> already, can you?

Neverbirth commented 7 years ago

You mean we should support casting to GenericClass from GenericClass in some way?

Not exactly. I mean if you have some external assembly expecting a GenericClass<CustomClass> instance, but Haxe is generating for your code GenericClass<object> your code won't work. I don't know if right now there is some workaround for this in place (you could use LINQ Cast() or OfType() functions for example), but if so I'd guess it would fail when using reflection for example.

I guess the same happens if you generate public API with Haxe? You want to generate an assembly that accepts GenericClass<CustomClass>, but Haxe exposes GenericClass<object>, is this so? Not very nice :S.

vroad commented 7 years ago

Not so nice, but it seems impossible to GenericClass<CustomClass> without knowing about underlying type. You'll need to cast to GenericClass<CustomClass> in some way.

vroad commented 7 years ago

But it's not possible to cast GenericClass<int> to GenericClass<object> either. Then is it meaningful to suppot GenericClass<CustomClass> to GenericClass<object>? So, this is not for supporting cast to GenericClass<object>??

I've tried writing a function that returns System.Collections.Generic.List<CustomClass>. In this case the function returned List<CustomClass>. So this only applies to Haxe-created classes?

Neverbirth commented 7 years ago

I'm not sure to follow you :/. Having some fictitious 3rdparty.dll that you reference in your HxCS project, and 3rdparty.dll has a class NameSpace.ClassA that you need to use, with a IComparable<ClassA> Comparer property. You may have in your Haxe project something like class Test<T> implements IComparable_1<T>.

When you do new ClassA().Comparer = new Test<ClassA>() it gets converted to something similar to:

new global::NameSpace.ClassA().Comparer = ((global::System.IComparable<global::NameSpace.ClassA>) (new global::_Main.Test<object>()) );

Which AFAIK, should fail.

Neverbirth commented 7 years ago

Just tested it. Indeed it fails. Note that my sample is not 100% valid tho, because I was using IComparable, and from .NET 4 onward it's using variance (it's defined as public interface IComparable<in T> in there)... if we use a normal interface that isn't defining any variance (like I said some comments above) it's failing. I could upload a sample if desired.

vroad commented 7 years ago

I've found the place where this conversion is done.

https://github.com/HaxeFoundation/haxe/blob/9ab880c6bae5a0c0c234e8561eac6480938fbf26/src/generators/gencs.ml#L955

Changing dynamic_anon to t disabled type parameter conversion. However, as I mentioned , this will cause problems with GenericClass<Dynamic> because you cannot convert GenricClass<CustomClass> to GenericClass<Dynamic>.

@Neverbirth your test class is just a haxe-generated class that implements builtin .NET interface, so haxe still converts type parameters to object I guess.

Should we be able to convert GenricClass<CustomClass> to GenericClass<Dynamic>, in the first place? I rarely use this technique, but probably we should support this in some way because other targets support this.

vroad commented 7 years ago

Could this be fixed without converting all type parameters to object when https://github.com/HaxeFoundation/haxe/issues/4475 is implemented?

Neverbirth commented 7 years ago

Oh, I didn't consider how GenericClass<Dynamic> works in Haxe. Tricky indeed... I can think of workarounds for a lot of cases, but not to all of them... never had to thought of a case like this... Personally, if I would have to choose between this and keeping class definitions in generics I would choose the second tho.

EDIT: I guess typedefs can also become a bit tricky... didn't check the generated code for a case like this.

Anyway, all this topic is not a showstopper like the second bug I mentioned in my original report.

kLabz commented 5 years ago

Seems to be fixed by #8387 (or is there something else to do here?)