Generics are broken with non-pointer types
@ban
Submitted by Colomban Wendling Link to original bug (#774713)
Description
Generic support for fundamental value types like int, long, double, float, or even enum and flags is currently broken when sizeof(gpointer) != sizeof(G)
.
The problem is easy to understand: Vala uses gpointer
as the C type for generics, but fundamental value types do not necessarily work with that.
There are good examples of the problem e.g. in libgee's https://bugzilla.gnome.org/show_bug.cgi?id=597737 and https://bugzilla.gnome.org/show_bug.cgi?id=774669.
Even simple code like this will have problem if sizeof(G) > sizeof(gpointer)
, as the value will be truncated:
G value_generic<G>(G x) {
return x;
}
calling value_generic<int64>(4200000000000000000)
on a 32 bits architecture will not result in the expected 4200000000000000000
. FWIW, with GCC's -m32 on a Linux x86_64, it gives me 1487142912
.
However, this part isn't too problematic, as it only applies for types larger than gpointer
, and, on x86, only when the value actually is larger: value_generic<int64>(42)
actually works as the truncation doesn't lose anything relevant.
But it gets real problematic when doing more subtle things, like libgee's Collection.to_array(): creating an array of Gs:
G[] array_generic<G>(G x) {
G[] y = new G[2];
y[0] = x;
y[1] = x;
return y;
}
The problem is that it is expected that using array_generic<int>
() would be the same as replacing every occurrence of G
in the definition with int
. But it is not, it actually is gpointer
, so the function creates an array of gpointer
s, which on an x86_64 Linux doesn't have the same layout as an array of int
s (int
s being 4 bytes, and pointers 8). So the array returned by array_generic<int>()
cannot be used as an int[]
. Worse, if the generic type is larger than gpointer
, it'll result in a too small array the caller will happily access outside its actual bounds.
libgee worked around some of the issue in its Collection.to_array()
implementation by special-casing most fundamental value types (char
, uchar
, int
, uint
, etc.), and providing an implementation actually working on those specific types -- see https://git.gnome.org/browse/libgee/tree/gee/collection.vala#n158 However, they missed enumerations and flags -- see https://bugzilla.gnome.org/show_bug.cgi?id=774669 -- which are actually trickier to support (see below).
I unfortunately don't see a proper solution for the issue, at least not one that would be mostly compatible and fix everything. Maybe using GValues
for passing the generics by value would slightly help. Or always use the largest possible type for them instead of gpointer
?
For the array part, it could probably be hacked around manually playing with the memory: all it really requires is knowing the size of the type, and probably the layout of the actually used type in memory (gpointer
currently). Then, generics could use convoluted logic like that:
gpointer* array_generic (GType T, ..., gpointer x) {
guint8* result = g_malloc0_n (n, size_of_T);
memcpy(result + size_of_T*0, (guint8*) x, size_of_T);
memcpy(result + size_of_T*1, (guint8*) x, size_of_T);
return (gpointer*) result;
}
so, so long as the caller casts it back to the real type, it would work -- unless size_of_T
is larger than gpointer
, again. Reading x
might require some slightly more convoluted code though if the relevant part of the value isn't packed at the start of the gpointer
, but that would be doable too knowing enough about the platform I guess.
Another possible simpler/safer solution would be doing the same as libgee's in the compiler itself: emit specialized versions to be used for basic types. It would result in larger generated code, but it might be worth it. But it still doesn't really work good when sizeof(G) > sizeof(gpointer)
as some truncation will occur passing some data as gpointer
s (here, the x
argument).
Supporting enumerations and flags is even trickier, because their actual type size is trickier to know. GCC will use int
when it can, but will happily switch to a larger type if the enumeration has values larger than what an int
can represent. And this means we need to know the actual size of the enumeration to properly support that. Would it be a property on GEnumClass it would be easy. But unfortunately it isn't, and worse, GEnumClass doesn't actually really support enums larger than int
(as minimum and maximum are int
s, it's impossible to even guess the required size).
So… maybe it's good enough to support them only when their size == sizeof(int)
? Not sure, but at least it would work in most cases.
In the end, I guess the most realistic short-term solution would be using something like libgee's approach. But I have no good idea for a perfect long-term solution.
Version: 0.34.x