Currently, we represent Variant in C# with the object type (System.Object). We convert Variant values to the managed type that best matches the Variant type and return the result as object.
This has the following downsides:
- Converting to and from native Variant results in boxing of basic value types (like
int, or Vector2).
- Converting
object to a native Variant (ConvertManagedObjectToVariant):
2.1. Requires runtime type checking.
2.2. In the case of System.Collections.Generic types, we need to box value type items.
2.3. Easy to attempt to create/marshal a Variant with an unsupported type. It's not obvious which types are supported and this results in a nil Variant and a runtime error.
- Converting a native Variant to
object (ConvertVariantToManagedObjectOfType):
3.1. Requires runtime type checking.
3.2. Requires reflection when dealing with generic types (e.g., to access the constructor dynamically).
- Confusing
InvalidCastException when casting the object to int.
Variant stores 64-bit integers, so the boxed value is of type long. You can't cast a boxed long to int. You need to unbox it first and then do the cast:
object boxed = (long)10;
_ = (int)boxed; // Exception. Not casting, rather unboxing with the wrong type.
_ = (int)(long)boxed; // Good. Unbox with the right type and then cast.
I propose solving these problems with the addition of a C# Variant type that replaces object in this regard.
Layout
public struct Variant
{
short _type;
godot_variant_basic_data _data;
object _obj;
}
The _data field is a union that stores all basic value types. That is, types that do not require disposal:
// Variant types that don't need disposal
Bool, Int, Float
Vector2, Vector2i
Rect2, Rect2i
Vector3, Vector3i
Plane
Quaternion
Color
Rid
✔️ We don't need to box the values in the _data field (1.), it's a union of value types.
The content of the _obj field depends on whether we want lazy marshaling or not.
Lazy marshaling
We assign the _obj field with a wrapper containing the native Variant itself. No marshaling is done until the user actually attempts a type conversion.
This results in an extra allocation (the wrapper) if a conversion is eventually requested.
The wrapper (like other Godot type disposables) also has the overhead of needing to be registered/unregistered for disposal on hot-reload and app exit.
This version wins when dealing with many Variants whose value ends up not being accessed. I don't think this is very common, so it doesn't seem to be worth the downsides.
Eager marshaling
Currently, we represent Variant in C# with the
objecttype (System.Object). We convert Variant values to the managed type that best matches the Variant type and return the result asobject.This has the following downsides:
int, orVector2).objectto a native Variant (ConvertManagedObjectToVariant):2.1. Requires runtime type checking.
2.2. In the case of
System.Collections.Generictypes, we need to box value type items.2.3. Easy to attempt to create/marshal a Variant with an unsupported type. It's not obvious which types are supported and this results in a nil Variant and a runtime error.
object(ConvertVariantToManagedObjectOfType):3.1. Requires runtime type checking.
3.2. Requires reflection when dealing with generic types (e.g., to access the constructor dynamically).
InvalidCastExceptionwhen casting theobjecttoint.Variant stores 64-bit integers, so the boxed value is of type
long. You can't cast a boxedlongtoint. You need to unbox it first and then do the cast:I propose solving these problems with the addition of a C# Variant type that replaces
objectin this regard.Layout
The
_datafield is a union that stores all basic value types. That is, types that do not require disposal:✔️ We don't need to box the values in the
_datafield (1.), it's a union of value types.The content of the
_objfield depends on whether we want lazy marshaling or not.Lazy marshaling
We assign the
_objfield with a wrapper containing the native Variant itself. No marshaling is done until the user actually attempts a type conversion.This results in an extra allocation (the wrapper) if a conversion is eventually requested.
The wrapper (like other Godot type disposables) also has the overhead of needing to be registered/unregistered for disposal on hot-reload and app exit.
This version wins when dealing with many Variants whose value ends up not being accessed. I don't think this is very common, so it doesn't seem to be worth the downsides.
Eager marshaling