Jump to page: 1 2 3
Thread overview
July 28

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:

  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 mixins 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);
  1. 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 enums, where they can use it in almost every other place in the language.

July 29

On Sunday, 28 July 2024 at 22:20:47 UTC, Witold Baryluk wrote:

This is the same thing?

https://issues.dlang.org/show_bug.cgi?id=24666

July 29

On Sunday, 28 July 2024 at 22:20:47 UTC, Witold Baryluk wrote:

>

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,
}

Possible workaround if each enum member is given a specific initializer:

struct FooEnum
{
    int A = 5, B = 6;
    version (x86_64) {
        int C = 7;
    } else version (AArch64) {
        int C = 17;
    } else {
        static assert(0);
    }
}

mixin enumGen!(FooEnum, "FOO");

static assert(FOO.A == 5);
static assert(FOO.B == 6);

version (x86_64)
static assert(FOO.C == 7);

template enumGen(T, string name)
{
    private string _gen()
    {
        T v;
        auto r = "enum " ~ name ~ " {";
        foreach (m; __traits(allMembers, T))
        {
            import std.conv;
            r ~= m ~ "=" ~ __traits(getMember, v, m).to!string ~ ",";
        }
        r ~= "}";
        return r;
    }
    mixin(_gen);
}
July 30

On Monday, 29 July 2024 at 11:25:45 UTC, Nick Treleaven wrote:

>

Possible workaround if each enum member is given a specific initializer:

struct FooEnum
{
    int A = 5, B = 6;
    version (x86_64) {
        int C = 7;
    } else version (AArch64) {
        int C = 17;
    } else {
        static assert(0);
    }
}

mixin enumGen!(FooEnum, "FOO");

Of course, the mixin there is not doing anything more than a struct with enum members:

version = x86_64;

struct FOO
{
    enum A = 5, B = 6;
    version (x86_64) {
        enum C = 7;
    } else version (AArch64) {
        enum C = 17;
    } else {
        static assert(0);
    }
}

static assert(FOO.A == 5);
static assert(FOO.B == 6);

version (x86_64)
static assert(FOO.C == 7);

But potentially the mixin approach with int members could do more - e.g. auto increment any int members without initializer (i.e. whose value is zero), based on the previous lexical member initializer.

July 30

I’ve recently made a API for doing this in BindBC-Common. You can see it on the latest commit of my repository here. This is not tagged as a version yet, but you can download it & use dub add-local until I tag it.

It’s designed for making language bindings (mostly to C) so there’s a convenient way to have a D version of the enum (e.g. Plant.flower) and a C version (e.g. PLANT_FLOWER) with the same code. And you can turn them on and off easily so that people who don’t like one of the styles are still happy.
Usage is like…

import bindbc.common;

mixin(makeEnumBindFns(cStyle: true, dStyle: true)); //this defines `makeEnumBind`

//makes `enum Plant: ulong`:
mixin(makeEnumBind(q{Plant}, q{ulong}, members: (){
  EnumMember[] ret = [
    {{q{flower}, q{PLANT_FLOWER}}, q{1}},
    {{q{bush},   q{PLANT_BUSH}},   q{2}},
  ];
  if(condition){
    EnumMember[] add = [
      {{q{herb},   q{PLANT_HERB}},   q{4}},
    ];
    ret ~= add;
  }
  return ret;
}()));

Please note that some parameters and fields are required to be referred to by name. Read the documentation comments for makeEnumBind, EnumMember, and EnumIden to avoid misusing them.

July 31

On Tuesday, 30 July 2024 at 23:26:58 UTC, IchorDev wrote:

>

I’ve recently made a API for doing this in BindBC-Common. You can see it on the latest commit of my repository here. This is not tagged as a version yet, but you can download it & use dub add-local until I tag it.

It’s designed for making language bindings (mostly to C) so there’s a convenient way to have a D version of the enum (e.g. Plant.flower) and a C version (e.g. PLANT_FLOWER) with the same code. And you can turn them on and off easily so that people who don’t like one of the styles are still happy.
Usage is like…

import bindbc.common;

mixin(makeEnumBindFns(cStyle: true, dStyle: true)); //this defines `makeEnumBind`

//makes `enum Plant: ulong`:
mixin(makeEnumBind(q{Plant}, q{ulong}, members: (){
  EnumMember[] ret = [
    {{q{flower}, q{PLANT_FLOWER}}, q{1}},
    {{q{bush},   q{PLANT_BUSH}},   q{2}},
  ];
  if(condition){
    EnumMember[] add = [
      {{q{herb},   q{PLANT_HERB}},   q{4}},
    ];
    ret ~= add;
  }
  return ret;
}()));

Please note that some parameters and fields are required to be referred to by name. Read the documentation comments for makeEnumBind, EnumMember, and EnumIden to avoid misusing them.

All nice and dandy. I know how to write mixins, there are many ways to do it. I am just surprised language feature of version and static if that work so easily in many other places, cannot be used for anything useful in enums and enums only.

Is it really like so rare that it is not in the language?

Usually one would not want to define an enum that have different values and enum members, as this can mess other code. But then it is pretty normal when interfacing with other languages and systems, or even doing things like changing some default values depending on a version.

Phobos (especially core and sys and stdc) with free standing enums, partially due to this problem.

I do like idea of the struct with static enum members. It might just work, without CTFEs and mixins. I will give it a try.

July 31

On Tuesday, 30 July 2024 at 19:32:45 UTC, Nick Treleaven wrote:

>

On Monday, 29 July 2024 at 11:25:45 UTC, Nick Treleaven wrote:

>

Possible workaround if each enum member is given a specific initializer:

struct FooEnum
{
    int A = 5, B = 6;
    version (x86_64) {
        int C = 7;
    } else version (AArch64) {
        int C = 17;
    } else {
        static assert(0);
    }
}

mixin enumGen!(FooEnum, "FOO");

Of course, the mixin there is not doing anything more than a struct with enum members:

version = x86_64;

struct FOO
{
    enum A = 5, B = 6;
    version (x86_64) {
        enum C = 7;
    } else version (AArch64) {
        enum C = 17;
    } else {
        static assert(0);
    }
}

static assert(FOO.A == 5);
static assert(FOO.B == 6);

version (x86_64)
static assert(FOO.C == 7);

But potentially the mixin approach with int members could do more - e.g. auto increment any int members without initializer (i.e. whose value is zero), based on the previous lexical member initializer.

The struct trick is pretty nice. I will give it a go. At least it should work for the time being.

I will try to type-safe it a bit more. I.e. so doing something like Foo.A | Foo.B results in some (compile time) constant with proper type and not bare int. So functions receiving these enums can properly detect pass of naked integers, and such.

This has a potential.

July 31

On Wednesday, 31 July 2024 at 00:50:59 UTC, Witold Baryluk wrote:

>

All nice and dandy. I know how to write mixins, there are many ways to do it.

I am just providing an existing solution that is tailored to language bindings and should see extensive use in the near future and will therefore be less likely to have bugs in the long run.

>

I am just surprised language feature of version and static if that work so easily in many other places, cannot be used for anything useful in enums and enums only.

Judging by the replies to my DIP ideas thread, I think it’s partly an issue with allowing a declaration in the middle of one type of comma-separated list when it is not permitted in all others:

int[] x = [1,2,3, version(X) 4 else 0];
void fn(int a, version(X) int b, int c){}

The options are really:

  1. We make a way of declaring enums with statements instead of a list, much like in Swift, which would also allow things like enum member functions; or
  2. We allow for conditional compilation declarations in all comma separated lists. I don’t see much use of this for function parameters outside of language bindings, but with array literals this could alleviate having to laboriously concatenate many items together, and with associative array literals this would be very useful since you can’t just concatenate them easily.

I’d love to see if there’s any interest in either of those possibilities.

>

Usually one would not want to define an enum that have different values and enum members, as this can mess other code. But then it is pretty normal when interfacing with other languages and systems, or even doing things like changing some default values depending on a version.

Yes, it’s mostly needed for making language bindings, so that means that most people don’t understand why you’d even want the feature; but as someone who creates a lot of language bindings, I absolutely see the utility.

>

I do like idea of the struct with static enum members. It might just work, without CTFEs and mixins. I will give it a try.

It’s a clever workaround but won’t work the way people want with a lot of enum-based meta-programming (e.g. std.conv.to). You should also @disable this();.

July 30
This comes up from time to time.

```
enum FOO {
   A = 5,
   B = 6,
version (x86_64) {
   C = 7,
} else version (AArch64) {
   C = 17,
} else {
   static assert(0);
}
   version E = 9,
}
```

I've seen this forever in C. It just makes my brain bleed. Here's the D way:

```
version (x86_64)
{
    enum FOO
    {
       A = 5,
       B = 6,
       C = 7,
    }
}
else version (AArch64)
{
   enum FOO
   {
       A = 5,
       B = 6,
       C = 17,
   }
}
else
    static assert(0);
```

Ah, doesn't that look nicer? It's nicer because the code forms a regular pattern. A regular pattern is easier to understand. It also uncovers errors, like the `version E = 9,` error in the opening example.

Also, when I'm debugging AArch64 code, I don't want to see x86_64 code interleaved in with it. Such makes it too easy to inadvertently mix them up.
July 31

On Wednesday, 31 July 2024 at 06:11:15 UTC, Walter Bright wrote:

>

Here's the D way:

version (x86_64)
{
    enum FOO
    {
       A = 5,
       B = 6,
       C = 7,
    }
}
else version (AArch64)
{
   enum FOO
   {
       A = 5,
       B = 6,
       C = 17,
   }
}
else
    static assert(0);

Ah, doesn't that look nicer? It's nicer because the code forms a regular pattern. A regular pattern is easier to understand.

Could you please rewrite the D version of this enum to use this pattern for me? When you’re done, post it as a reply so we can see how much nicer it looks. ;)

There’s only 14 different possible versions, and each one is at least 67 lines long (unless you inline the enum, in which case you can kiss readability goodbye), so it should only be a bit over 1000 lines of code when you’re finished there. Compared with the 238 lines of my original version—which could be further shortened if it was using conditional compilation instead of meta-programming—and it might become apparent that your suggestion forsakes the DRY principle.

« First   ‹ Prev
1 2 3