Static array auto calc length shipped in opend, ctfe chained exception bug fix, importc manifesto

Posted 2026-03-19

New feature shipped in OpenD

A long-requested feature upstream got implemented by someone targeting upstream, but it was easy to adapt to OpenD and we shipped it the day after it was implemented: static arrays with automatically calculated lengths.

int[$] a = [1,2,3];
static assert(a.length == 3); // same as if you wrote `int[3] a` above

Hardly revolutionary; perhaps not even ideal, since it only works on declarations and not expressions (though expressions can be used as static arrays automatically when used in particular contexts), but meh, it is another common-sense, incremental improvement to D that users have wanted for a long time.

CTFE bug fix

While using a graphql bug for work, I was frustrated by exceptionally awful error messages. Traced this to a druntime branch, added to D sometime around 2.100 and thus present in OpenD and upstream, that tried to do a void cast while chaining exceptions as part of a memory management scheme. At runtime, this worked, but at compile time, it erased the original error entirely and replaced it with one about that cast being illegal at compile time.

My fix is to check if(__ctfe) and concatenate strings instead. This, again, is not a perfect fix, since exception strings do not necessarily contain all the relevant information, but it is better than nothing.

An ImportC Manifesto

I have barely used importc (I just haven't found a right fit... it doesn't solve problems I have), but knowing a little about the implementation and D in general, I put a little thought into how I would use it, if I ever did. Here's some of those thoughts, but note they have not been tested in real world use.

The general idea is that all C use should be encapsulated in a private module of your package. You have exactly *one* .c file, and it #includes *everything* you need for your program in that one bridge file. Write any helper C functions you'll need in that file, for example, wrappers for things that use macros that D can't understand, and then wrap this C api in a D api externally. All types, even if coming from C with no or minimal wrapping, should be considered unique to your package.

Your C file should be part of your D package, using the __module definition in the bridge, and this C bridge file should *not* be part of your public api, generally speaking, because any C types it uses are liable to conflict outside. Consider, for example, FILE*. In C, this would come from #include <stdio.h>, but in D, it comes from import core.stdc.stdio; Despite referring to the same C struct, these are different types in D because of module namespacing. Any #include<stdio.h> from the C file, even if included by something else you included, introduces that symbol in the module namespace, which is liable to lead to errors like the following:

importcuser.d(6): Error: `stdout` matches conflicting symbols:
/usr/include/stdio.h(149):        variable `importcthing.stdout`
/import/std/stdio.d(5466): function `std.stdio.makeGlobal!"core.stdc.stdio.stdout".makeGlobal`

Even if you don't import the two modules together, you can still get problems:

importcuser.d(6): Error: function `importcthing.foo(_IO_FILE* fp)` is not callable using argument types `(shared(_IO_FILE)*)`
importcuser.d(6):        cannot pass argument `makeGlobal().getFP()` of type `shared(_IO_FILE)*` to parameter `_IO_FILE* fp`

The FILE* from import c and the FILE* from the D standard library refer to the same thing... but the compiler doesn't actually know that. This is why I say put all the C things you use in just one bridge module. Inside that file, it works by C rules, and will interop fine. Then, outside that file, you put a D wrapper around it (or use it directly as an implementation detail of your D application) that encapsulates this and provides your actual functionality.

By using the __module thing in your importc bridge, you can namespace this appropriately for an internal implementation detail of whatever it is you are actually trying to do. For your public interface, you may need to do some casts to anchor the C bridge back to the D world

import c = importcthing;

import std.stdio;

void foo(FILE* fp) {
	return c.foo(cast(c.FILE*) fp);
}

This would be a hassle to do for every single function, but when you're providing enhanced functionality, you're probably doing a little more than just this anyway.