enums and version/static if/"inheritance"
Witold Baryluk
witold.baryluk+d at gmail.com
Sun Jul 28 22:20:47 UTC 2024
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
```d
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:
```d
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:
```d
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):
```d
module ...linux.mman;
enum MMapFlags : imported!"...posix.mman").MMapFlags {
// some extra linux specific flags
MAP_DROPPABLE = 0x08,
MAP_UNINITIALIZED = 0x4000000,
}
```
and common ones:
```d
module ...posix.mman;
enum MMapFlags {
version (linux) {
MAP_PRIVATE = 0x02,
}
// ...
}
```
So only options are:
1. 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.
2. Create some kind of string DSL, CTFEs and `mixin`s to build
the full enum definition, something like this possible.
```d
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.
```d
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);
```
3. Use typedef, to create new distinct type for these various
flag constants, and use that.
Maybe
```d
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.
More information about the Digitalmars-d
mailing list