Beyond the veil: What's after type functions

Stefan Koch uplink.coder at googlemail.com
Mon Jan 4 19:30:31 UTC 2021


Good Evening,

I recently had a programming task in my hobby project where I 
needed to interface with a C library that uses manual closures 
using void**.
This quickly lead to bugs where one would forget to deref twice 
for read or accidental double deference on write (causing the 
written value to be invisible).

After that casting to the correct structure was an issue, since 
(void**) trows away typing information.

So I wanted a discriminated union (a "sumtype") to which I would 
use for all closures where the discriminant would tell me the 
rumtime type.

This is what I ended up with:

---
struct UploadClosure
{
     MHD_PostProcessor* pp;
     const (char)* filename;
     FILE* f;
     size_t totalSize;
}

struct KeyServerClosure
{
     bool processing_request;
}

enum ConnectionClosureKind
{
     Invalid,
     UploadClosure,
     KeyServerClosure,
}

ConnectionClosure* newClosure(ConnectionClosureKind kind)
{
     alias Kind = ConnectionClosureKind;
     const closureSize = closureSize(kind);

     ConnectionClosure* result = /*new ConnectionClosure(); //*/ 
cast(ConnectionClosure*)new ubyte[](closureSize).ptr;

     result.kind = kind;

     final switch (kind)
     {
         case Kind.UploadClosure :
         {
             // result.uploadClosure = new UploadClosure();
             result.uploadClosure = cast(UploadClosure*) 
((cast(void*)result) + (*result).sizeof);
         } break;

         case Kind.KeyServerClosure : {
             result.keyServerClosure = cast(KeyServerClosure*) 
((cast(void*)result) + (*result).sizeof);
         } break;

         case Kind.Invalid : {};
     }

     // import std.stdio; writeln(*result);

     return result;
}

struct ConnectionClosure
{
     ConnectionClosureKind kind;
     union
     {
         UploadClosure* uploadClosure;
         KeyServerClosure* keyServerClosure;
     }
}

size_t closureSize (ConnectionClosureKind kind) pure nothrow @nogc
{
     size_t result = ConnectionClosure.sizeof;

     final switch (kind)
     {
         case ConnectionClosureKind.UploadClosure : result += 
UploadClosure.sizeof;
         case ConnectionClosureKind.KeyServerClosure : result += 
KeyServerClosure.sizeof;
         case ConnectionClosureKind.Invalid : {}
     }

     return result;
}
---

You can easily Imagine the 'struct ConnectionClosure' and the 
`new Closure` function to be generate from the `enum 
ConnectionClosureKind` which would reduce the code snippet I 
posted by 54 lines (which is over 70% of the 75 line snippet).

Traditionally in D one would use templates and static foreach, 
which are known to scale awfully with regards to compile time, 
once you base an entire system on them.
Also they will most likely use string mixins in the 
implementation which means you can run into visibility/protection 
and namespace issues.

So I started to sketch what I would want to the compiler to do. 
(And I know it can do it, because these operations are performed 
internally)

And here was what this would look like:

---
alias type = __type__;

/// Compiler interal API looks up a typename in the given 
scope($D lookupScope)
/// Returns: the type named ($D typeName) if found
///          __emptyType otherwise.
type __lookupTypeFromScope (string typeName, __scope lookupScope);

/// compiler internal API returns the current scope
/// Returns: the current scope
__scope __currentScope();

bool hasInvalidMember(type Enum)
{
     static immutable members = __traits(allMembers, Enum);
     return members.length && members[0] == "Invalid";
}

/// given an enum ($ closureEnumType) where all members are 
type-name (execpt for the first member iff that member is called 
'Invalid' return an array of type values which corrospond the 
type names in the enum. By default the names are looked up in the 
current scope, but this can be customized setting optional the 
lookupScope parameter)
type[] getClosureTypes(type closureEnumType, __scope lookupScope 
= __currentScope())
{
     assert(is(closureEnumType == enum), __FUNCTION__ ~ " expects 
an enum to be passed in" ~
         " which has member named equivalent to the types stored 
in the closure union");

     // type function traits don't produce compiler tuples since 
that would mean a shape change.
     immutable string[] enumMembers =
         __traits(allMembers, 
closureEnumType)[hasInvalidMember(closureEnumType)
                  /*strip the first invalid member if there is 
one*/ .. $];

     __type__[] result;
     result.length = enumMembers.length;

     foreach(i,m;enumMembers)
     {
         __type__ t = __lookupTypeFromScope(m, lookupScope);
         // assert that t is not the __emptyType
         debug { assert(is(t), "Could not find a type named: " ~ 
m); }
         else { if (!is(t))  return null; }
         result[i] = t;
     }

     return result;
}

string genSizeEnumerationCases(type closureEnumType, string 
targetVaribleName)
{
     string result;
     auto closureTypes = getClosureTypes(closureEnumType, 
__currentScope());
     assert(closureTypes != null,
         "An error occurred when getting closure types, perhaps 
the types are not visible from current scope");

     immutable string case_prefix = "case " ~ __traits(identifier, 
closureEnumType) ~ ".";
     foreach(t;closureTypes)
     {
         result ~= case_prefix ~ __traits(identifier, t) ~ ": " ~ 
targetVaribleName ~ " = " ~ t.sizeof ~ "; break;\n";
     }

     if (hasInvalidMember(closureEnumType))
         result ~= case_prefix ~ "Invalid : " ~ targetVaribleName 
~ " = 0;\n";
}

size_t closureSize (ConnectionClosureKind kind) pure nothrow @nogc
{
     size_t result = ConnectionClosure.sizeof;

     size_t size;

     final switch (kind)
     {
         mixin(genSizeEnumerationCases(typeof(kind), "size"));
     }

     result += size;

     return result;
}
---

Look at the concise imperative code above.
It does not rely on any library functionality (except if you want 
to call the compiler internal api a 'library').

Now let's write a template which would do the same thing trying 
to reduce compile times as much as we can:

---
template hasInvalidMember(EnumT)
{
     enum hasInvalidMember = __traits(allMembers, EnumT).length > 
1 && __traits(allMembers, EnumT)[0] == "Invaild";
}

/// Note: Be careful about the instantiation context.
/// This can go very poorly of the type names in EnumT are 
locally shadowed, we can't pass a scope in here so we can't fix 
this!
template getClosureTypes(EnumT)
{
     alias getClosureTypes = mixin(() {
     char[] result = cast(char[])"AliasSeq!("; // let's hope this 
template is defined in the destination of the mixin otherwise we 
will error
     // note: we could fix this my nesting a couple more mixins 
but then the code get's even more bloated
     static assert(is(EnumT == enum), __FUNCTION__ ~ " expects an 
enum to be passed in" ~
         " which has member named equivalent to the types stored 
in the closure union");
     // this will be a statically unrolled tuple foreach bloating 
the size of this
     // since we need to do another mixin within here
     // this might be removed before codegen but you do pay the 
price of building the function before codegen even though it's 
only use once by ctfe.
     // and it will be one function per instance of 
getClosureTypes bloating the internal symbol table.
     foreach(i,m;__traits(allMembers, 
enumT)[hasInvalidMember!(enumT) .. $])
     {
         static assert(is(mixin(m)), m ~ " is either not a type, 
or it's not visible here");
         result ~= m ~ ",";
     }
     // replace the last useless ',' with a ')' to close the 
AliasSeq.
     result[$-1] = ')';
} ());
}

template genSizeEnumerationCases(closureEnumType, string 
targetVaribleName)
{
     enum genSizeEnumerationCases = (() {
     string result;
     alias closureTypes = getClosureTypes!(closureEnumType);
     // also note how we can't assert here that getClosureTypes 
not error ...
     // thereby lengthening the instantiation trace if something 
does go wrong
     immutable string case_prefix = "case " ~ __traits(identifier, 
closureEnumType) ~ ".";
     foreach(t;closureTypes)
     // again unrolled tuple foreach bloating this function ...
     // and again we actually have to build an internal function 
and then we have to interpret it. So we pay for the template 
instance and for CTFE...
     {
         result ~= case_prefix ~ __traits(identifier, t) ~ ": " ~ 
targetVaribleName ~ " = " ~ t.sizeof ~ "; break;\n";
     }

     if (hasInvalidMember!(closureEnumType))
         result ~= case_prefix ~ "Invalid : " ~ targetVaribleName 
~ " = 0;\n";

      return result;
}());
}

size_t closureSize (ConnectionClosureKind kind) pure nothrow @nogc
{
     size_t result = ConnectionClosure.sizeof;

     size_t size;

     final switch (kind)
     {
         mixin(genSizeEnumerationCases!(typeof(kind), "size"));
     }

     result += size;

     return result;
}
---

Without the comments the template would probably be somewhat 
shorter.
But again they are dependent on the external template AliasSeq to 
be visible in the template instantiation context.
CTFE can't cache them because they are new symbols every time.
They use unrolled tuple foreaches increasing the size of the body 
that ctfe and semantic has to deal with.
And they have to generate new symbols persistently ... even 
though those might get stripped the compiler has to allocate 
memory for them and spent time to generate them.
Thereby memory consumption and further reducing scalability.

whereas the type function with extended internal compiler API has 
none of these problems.
And does not rely on library functionality.

With this in mind, please tell me what you think :)

Cheers,
Stefan


More information about the Digitalmars-d mailing list