Storing interfaces as void[]

tsbockman thomas.bockman at gmail.com
Fri Mar 12 22:18:59 UTC 2021


On Friday, 12 March 2021 at 19:24:17 UTC, David  Zhang wrote:
> On Friday, 12 March 2021 at 18:50:26 UTC, tsbockman wrote:
>> <snip>
>
> The idea is to implement a service locator s.t. code like this 
> is possible:
> ...
> I don't really need to copy or move the class instances here, 
> just be able to read, assign, and replace references to them in 
> the same place that I read, assign, and replace void[]'s of 
> structs.

Why do you think you need a `void[]` slice? I think `void*` 
pointers are sufficient. This handles all normal data types, as 
long as they are allocated on the GC heap:

////////////////////////////////
/* NOTE: It might be wiser to make this a `final class`
instead of a `struct` to get reference semantics: */
struct Registry {
     private void*[TypeInfo] map;

     /// Return true if the registry was updated, false if not.
     bool put(bool overwrite = true, Indirect)(Indirect i) @trusted
     	if(is(Indirect : T*, T) // Support structs, static arrays, 
etc.
         || is(Indirect == interface) || is(Indirect == class))
     {
         auto key = typeid(Indirect), value = cast(void*) i;
         bool updated;
         if(value is null)
             updated = map.remove(key);
         else {
             static if(overwrite) {
                 updated = true;
                 map[key] = value;
             } else
                 updated = (map.require(key, value) is value);
         }
         return updated;
     }
     alias put(Indirect) = put!(true, Indirect);

     bool remove(Indirect)() @trusted
     	if(is(Indirect : T*, T) // Support structs, static arrays, 
etc.
         || is(Indirect == interface) || is(Indirect == class))
     {
         return map.remove(typeid(Indirect));
     }

     /** Returns a reference (for interfaces and classes) or
     a pointer (for everything else) if the type has been 
registered,
     and null otherwise. **/
     Indirect get(Indirect)() @trusted
         if(is(Indirect : T*, T) // Support structs, static 
arrays, etc.
         || is(Indirect == interface) || is(Indirect == class))
     {
         return cast(Indirect) map.get(typeid(Indirect), null);
     }
}
@safe unittest {
     static interface I {
         char c() const pure @safe nothrow @nogc;
     }
     static class C : I {
         private char _c;
         this(char c) inout pure @safe nothrow @nogc {
             this._c = c; }
         override char c() const pure @safe nothrow @nogc {
             return _c; }
     }
     static struct S {
         char c = 'S';
         this(char c) inout pure @safe nothrow @nogc {
             this.c = c; }
     }

     Registry registry;
     assert( registry.put!false(new C('1')));
     assert( registry.put!I(new C('2')));
     assert( registry.put(new S('$')));
     assert(!registry.put!false(new C('3'))); // Optionally 
protect existing entries.

     assert(registry.get!(I).c == '2');
     assert(registry.get!(C).c == '1');
     assert(registry.remove!I);
     assert(registry.get!I  is null);
     assert(registry.get!C !is null);

     assert(registry.get!(S*).c == '$');
     assert(registry.get!(int*) is null);
}
////////////////////////////////

NOTE: If you only need one Registry instance in per thread, then 
you probably don't need the associative array at all; instead 
just use a template like this:

////////////////////////////////
template registered(Indirect)
     if(is(Indirect : T*, T) // Support structs, static arrays, 
etc.
     || is(Indirect == interface) || is(Indirect == class))
{
     /* This uses thread local storage (TLS). Sharing across the 
entire
     process is possible too, but would require synchronization of 
the
     registered objects, not just this reference/pointer: */
     private Indirect indirect = null;

     /// Return true if the registry was updated, false if not.
     bool put(bool overwrite = true)(Indirect i) @safe
     {
         bool updated = (indirect !is i);
         static if(overwrite) {
             indirect = i;
         } else {
             updated &= (indirect is null);
             if(updated)
             	indirect = i;
         }
         return updated;
     }

     bool remove() @safe {
         bool updated = (indirect !is null);
         indirect = null;
         return updated;
     }

     /** Returns a reference (for interfaces and classes) or
     a pointer (for everything else) if the type has been 
registered,
     and null otherwise. **/
     Indirect get() @safe {
         return indirect; }
}
@safe unittest {
     static interface I {
         char c() const pure @safe nothrow @nogc;
     }
     static class C : I {
         private char _c;
         this(char c) inout pure @safe nothrow @nogc {
             this._c = c; }
         override char c() const pure @safe nothrow @nogc {
             return _c; }
     }
     static struct S {
         char c = 'S';
         this(char c) inout pure @safe nothrow @nogc {
             this.c = c; }
     }

     assert( registered!(C).put!false(new C('1')));
     assert( registered!(I).put(new C('2')));
     assert( registered!(S*).put(new S('$')));
     assert(!registered!(C).put!false(new C('3'))); // Optionally 
protect existing entries.

     assert(registered!(I).get.c == '2');
     assert(registered!(C).get.c == '1');
     assert(registered!(I).remove());
     assert(registered!(I).get  is null);
     assert(registered!(C).get !is null);

     assert(registered!(S*).get.c == '$');
     assert(registered!(int*).get is null);
}
////////////////////////////////

> Aye, I'm using hashes. The idea is to support either D 
> interfaces or structs with arbitrary content.

You can use TypeInfo references as the keys for the struct types, 
too. It's better than hashes because the TypeInfo is already 
being generated anyway, and is guaranteed to be unique, unlike 
your hashes which could theoretically collide.

>> `I` is *not* the type of an interface instance, it is the type 
>> of a reference to an instance of the interface.
>
> So `I i` is a reference to the instance, which itself holds a 
> reference to the implementation, like a reference to a delegate 
> (pointer to (ctx, fn))?

No. What you are calling "the implementation" is the same thing 
as "the instance". `I i` is a reference into the interior of the 
implementing class instance, offset such that it points at the 
virtual function table pointer for that interface. The lowering 
is something like this:

////////////////////////////////
interface I { ... }
class C : I { ... }
struct C_Instance {
     void* __vtblForC;
     // ... other junk
     void* __vtblForI;
     // ... explicit and inherited class fields, if any.
}

C c = new C; // Works like `C_Instance* c = new C_Instance;`
I i = c; // Works like `void** i = 
(C_Instance.__vtblForI.offsetof + cast(void*) c);`
////////////////////////////////

You can prove this to yourself by inspecting the raw numerical 
addresses. You will find that the address used by the interface 
reference points to a location *inside* the instance of C:

////////////////////////////////
void main() {
     import std.stdio : writeln;

     static interface I { char c() @safe; }
     static class C : I { override char c() @safe { return 'C'; } }

     C c = new C;
     const cStart = cast(size_t) cast(void*) c;
     const cEnd = cStart + __traits(classInstanceSize, C);
     I i = c;
     const iStart = cast(size_t) cast(void*) i;
     writeln(cStart <= iStart && iStart < cEnd);
}
////////////////////////////////

> Thus cast(void*) produces something like *(ctx, fn).

No. See above.


More information about the Digitalmars-d-learn mailing list