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