I do often develop some low level codes, often interacting with Linux kernel, or some embedded systems. Where unfortunately some definitions of various constants and interfaces vary vastly between architectures or oses, or compilation options.
My today complain would be with enums
.
A good context is just checking various mman.d
files in phobos, where a number of top level enum
values are defined using mix of version and static ifs, or even a bit of logic for fallback.
Unfortunately this only works for top level enum values. It does not work for named enum types.
A simple example
enum FOO {
A = 5,
B = 6,
version (x86_64) {
C = 7,
} else version (AArch64) {
C = 17,
} else {
static assert(0);
}
version E = 9,
}
// static assert(checkUniqueValues!FOO);
Is simply not supported (not even that surprising considering that enum members are separated by comma, not a semicolon).
Same with static if
.
Doing enum FooBase { ... } version () ... enum FOO : FooBase { ... }
will not work (it does something else that expected)
One will say, how about this:
enum FOO {
A = 5,
B = 6,
C = ((){ version (x86_64) return 7; else return 17; })(),
E = 9,
}
// static assert(checkUniqueValues!FOO);
That is unfortunately not good enough, and very verbose. There are often multiple values that need to have these branching structure, and now one is forced to repeat it multiple time.
And even worse, this only allows defining enum members that only differ in value. One cannot use this method to decide if enum member/value exists or not (or maybe put @deprecated or other UDA on them conditionally).
Example might be on linux x86_64 one should only use HUGE_TLB_2MB
and HUGE_TLB_1GB
, but on ppc64, different values are allowed, like only HUGE_TLB_16MB
is allowed. In linux uapi / libc all values are defined on all archs, but using wrong ones will definitively will cause a runtime error, and it would be preferably that library just hides unsupported ones by default.
Another example might be enum with some values hidden because they are broken / deprecated, but some special version
could be used to enabled if one knows what they are doing (Example on linux might be some syscall numbers, which are deprecated or essentially broken, and superseded by better interfaces, example might be tkill
, or renameat
).
And last consideration, again for mmap
flags.
There is a set of mmap flags defined by posix (the actual values might different between systems and even archs). And a bunch of extra flags supported on various systems. In Phobos these is done by just having top level enum values (not part of any enum type), and importing proper modules with these values.
This makes impossible to write "type-safe" wrappers around mmap
(lets assume we are calling Linux kernel directly so we can ignore all glibc / musl stuff, and we can invent a new interface), and flags must be just int.
In ideal world, one would import ...linux.mman : mmap
, and it would have signature:
module ...linux.mman;
version (X86_64) {
void* mmap(void* p, size_t, MMapProt prot, MMapFlags flags, int fd, off_t off) {
return cast(void*)syscall!(Syscall.MMAP)(cast(ulong)p, cast(uint)prot, ...)
}
}
where for example MMapFlags would be (hypothetically):
module ...linux.mman;
enum MMapFlags : imported!"...posix.mman").MMapFlags {
// some extra linux specific flags
MAP_DROPPABLE = 0x08,
MAP_UNINITIALIZED = 0x4000000,
}
and common ones:
module ...posix.mman;
enum MMapFlags {
version (linux) {
MAP_PRIVATE = 0x02,
}
// ...
}
So only options are:
-
Redefine entire struct of the enum (including comments, and expressions, i.e. if some enum member values are expression, like
MAP_HUGE_1GB = 30U << HUGETLB_FLAG_ENCODE_SHIFT,
) for every possibly combination of system, architecture, and in some cases even libc. This does not scale well. -
Create some kind of string DSL, CTFEs and
mixin
s to build the full enum definition, something like this possible.
mixin(buildEnum(`
A 5
B 6
C 7 if x86_64
C 17 if AArch64
E 9
`));
And then provide some reflection based enum inheritance (where enum member values or their presence can be also be steered by some static ifs / version logic). Surely doable, but not very clean.
But it is pretty ugly and limiting, not to mention probably does not play well with most IDEs.
Maybe something like this would be interesting too.
enum FOO {
A = 5,
B = 6,
mixin("C = 7")
} else version (AArch64) {
C = 17,
} else {
static assert(0);
}
version E = 9,
}
// static assert(checkUniqueValues!FOO);
- Use typedef, to create new distinct type for these various flag constants, and use that.
Maybe
import std.typecons;
alias Foo = Typedef!int;
enum Foo B = 6;
But then the actual structure and discoverability is lost. Plus if one has two classes of values both having B
, they will clash, unless one puts some prefixes, or puts them in different modules, which is a big limitation.
I am not advocating for extending language necessarily, as I am sure library solution could be worked out too. Still it is a bit strange that one cannot easily use version
and static if
in enum
s, where they can use it in almost every other place in the language.