Closed mihe closed 11 months ago
Cc @clayjohn @dsnopek
Hm. GDExtension always encodes all integers as int64_t
, so I suspect this isn't a Godot issue, but a godot-cpp one?
I've never used this syntax, I just Google'd it up, but perhaps godot-cpp should be generating all enums as:
enum ArrayFormat : __int64 {
// ...
}
... to specify that 64-bit integers should be used for enums?
~Enum might be int
depending on platform I'm not sure, but it should generate some warning if it truncates I believe~
Apparently, based on the standard (according to cppreference):
this type is not larger than int unless the value of an enumerator cannot fit in an
int
orunsigned int
If the underlying type is not fixed, the value is convertible to the first type from the following list able to hold their entire value range:
int
,unsigned int
,long
,unsigned long
,long long
, orunsigned long long
, extended integer types with higher conversion rank (in rank order, signed given preference over unsigned)
This could of course be an MSVC issue as MSVC is infamous for the weird ways it fails to conform to the standard at times
Can you confirm this on other compilers such as clang? In testing clang does correctly handle enums of this scale, but can't test msvc right now
See https://github.com/godotengine/godot/pull/61757 where @lawnjelly dealt with a similar MSVC issue with enums.
Then this seems to be godot-cpp specific, should we transfer? As the bindings aren't directly affected AFAIK
There's three options I can see:
int64_t
as we sometime used signed enum values, but then what about if we actually start using uint64_t
enums in Godot with values beyond INT64_MAX
?MSVC does seem to be the outlier here. Clang and GCC both seem to handle this gracefully, whereas MSVC emits C4309 for both ARRAY_FLAG_FORMAT_VERSION_2
and ARRAY_FLAG_FORMAT_CURRENT_VERSION
and truncates to 0.
Either bindings like godot-cpp should default everything to a big enough type, e.g.
int64_t
as we sometime used signed enum values, but then what about if we actually start usinguint64_t
enums in Godot with values beyondINT64_MAX
?
While enums for the most part seem to be transported across the ABI as int64_t
, thanks to VARIANT_ENUM_CAST
and such, I've found at least one exception, which is the CaretInfo
struct, which happens to have TextServer::Direction
in it, as seen here:
So if Godot and godot-cpp start to differ on how big this particular enum is you'll run into trouble when reading/writing those variables.
I'm noticing now though that RenderingServer::ArrayFormat
is bound through VARIANT_BITFIELD_CAST
as opposed to VARIANT_ENUM_CAST
, which leads it to getting marked as "is_bitfield": true
in extension_api.json
. Maybe the pragmatic solution for this particular case is as simple as just adding : uint64_t
in godot-cpp's code generation when the enum is marked as a bit field?
I took the liberty of creating a PR (godotengine/godot-cpp#1320) for the solution mentioned in my previous comment, in case you just want to resolve the more immediate problem here without any changes to Godot itself.
There's obviously still a bigger discussion to be had though, with regards to being more explicit about the underlying types of enums.
I was just looking to see if there was a compiler switch to change the MSVC behaviour, but that doesn't protect against future compilers that might default to signed enum.
One option is to enforce all enums being unsigned via the static checks (in Godot core) and then force either uint32_t
or uint64_t
in godot cpp.
It of course only tends to be a problem when they are used as bitfields, but there's no super obvious automatic way of detecting this, so the same bug is likely to come up again and again periodically.
One option is to enforce all enums being unsigned via the static checks (in Godot core) and then force either
uint32_t
oruint64_t
in godot cpp.It of course only tends to be a problem when they are used as bitfields, but there's no super obvious automatic way of detecting this, so the same bug is likely to come up again and again periodically.
Seeing as how bitfields are bound through a separate macro this would be fairly easy to enforce with the help of std::underlying_type
and static_assert
.
Here's a quick proof-of-concept: https://github.com/godotengine/godot/compare/master...mihe:godot:enforced-enum-types
This will trigger a compilation error if something bound through VARIANT_ENUM_CAST
isn't of underlying type int
and if something bound through VARIANT_BITFIELD_CAST
isn't of underlying type uint64_t
.
The ideal solution here would however (in my opinion) be for this information to explicitly be part of extension_api.json
instead of being a handshake agreement between Godot and any extension language bindings. This information should be fairly easy to extract/enforce automatically thanks to std::underlying_type
a couple of if constexpr
checks that check to see whether the enum is int
or one of the fixed-width integer types.
I was just looking to see if there was a compiler switch to change the MSVC behaviour, but that doesn't protect against future compilers that might default to signed enum.
For future reference, here is the compiler flag.
It seems like they had to leave it out of the standard-conformance flag (/permissive-
) due to it being a breaking change with the Windows SDK.
The ideal solution here would however (in my opinion) be for this information to explicitly be part of
extension_api.json
instead of being a handshake agreement between Godot and any extension language bindings.
Personally, I think enum
s should be part of the GDExtension ABI simply as integers, rather than trying to bind them too closely to C++ enum
s.
Technically, GDExtension is supposed to be a "C API" (although, I think it's really more like a "C ABI", since we don't require that a C compiler is used to interact with GDExtension, just that a C compatible ABI is used), and in C the size of an enum
is even more ambiguous: the best I can tell via Google, C enum
s are always int
-sized and we have no way to explicitly specify the storage.
So, I think GDExtension should simply be specifying that an enum value is passed to/from GDExtension as a 64-bit integer (like all integers presently), and how that's actually realized in a particular language is up to the language binding. So, in godot-cpp we've decided to use C++ enum
s which means we have to contend with their quirks. In a theoretical C binding, maybe C enum
s would be used, or maybe #define
s? Each language would figure out how best to represent it.
The one issue is with GDREGISTER_NATIVE_STRUCT()
, but given that it's supposed to hold a C struct and C is ambiguous about the size of enum
s, I think we should probably have a rule against using enum
s in native structs.
Something good to discuss at the next GDExtension meeting! (I've already put it on the agenda :-))
and in C the size of an
enum
is even more ambiguous: the best I can tell via Google, Cenum
s are alwaysint
-sized and we have no way to explicitly specify the storage.
I'm not sure I agree with it being ambiguous. cppreference.com, which despite the name also provides a reference for the C language standard, seems quite clear about it using int
as its underlying type (up until C23, but I assume GDExtension targets something like C99). While int
isn't strictly guaranteed to be 32-bit, it should be 32-bit on any platform and architecture relevant to Godot that I'm aware of.
There are popular and widely used C libraries that expose enums as part of their interface just fine. libclang springs to mind as one that also has bindings for many other languages.
With that said, the fact that enums are already being transported across the ABI as int64_t
in every case except for in GDREGISTER_NATIVE_STRUCT()
might be reason enough to just rip the CaretInfo
compatibility breakage band-aid and explicitly declare them all to be 64-bit, as you say. Having the enum size provided as part of extension_api.json
and then still always have them be passed around as int64_t
would be somewhat strange, now that I think about it.
If the compatbility breakage is off the table, then the only other solution I can see is to enforce that all VARIANT_ENUM_CAST
enums are of underlying type int
(or int32_t
I guess) and enforce that all VARIANT_BITFIELD_CAST
enums are of underlying type uint64_t
, or that they're only ever used in GDREGISTER_NATIVE_STRUCT()
as BitField<T>
.
While
int
isn't strictly guaranteed to be 32-bit, it should be 32-bit on any platform and architecture relevant to Godot that I'm aware of.
Yeah, the undefined size of int
is what I meant by it being ambiguous. But, you're right, it's effectively always 32-bit :-)
Godot version
4.2-rc [b3a0e077e]
System information
Windows 11 (10.0.22621)
Issue description
With the new vertex compression introduced in #81138 the
RenderingServer::ArrayFormat
enum was changed to use an underlying type ofuint64_t
as opposed to the defaultint32_t
int
, as seen here:https://github.com/godotengine/godot/blob/eda44bfe109d7f15c5a965db2d65c8a17ce4b7d0/servers/rendering_server.h#L255-L299
As far as I can tell, changing the underlying type for an enum is not something that
extension_api.json
supports at the moment. This leads toArrayFormat
being generated as such in godot-cpp:This seems to result in the compiler (MSVC in this case) truncating any enum flag larger than 2^31 down to 0, while also emitting warnings about this, which means that actually using
ARRAY_FLAG_FORMAT_VERSION_2
orARRAY_FLAG_FORMAT_CURRENT_VERSION
, and thus conforming to this new vertex format without Godot emitting warnings, is impossible from godot-cpp without specifying the actual literal numerical value instead.Steps to reproduce
N/A
Minimal reproduction project
N/A