On Wednesday, 6 September 2023 at 18:54:26 UTC, Soham Mukherjee wrote:
> It would be helpful if you could provide code examples and explain how custom attributes can be leveraged in practical scenarios within D programming. Thank you for your insights!
If you'd like a more powerful example... here is a proof of concept for an RPC module that uses UDAs to allow easy definition of success or failure callbacks the called function can seamlessly reply with.
proxy.requestName().onSuccess((string str) {
writefln("We got the name reply back from the server: %s", str);
}).onFailure((string errMsg) {
writefln("Something went wrong! Error: %s", errMsg);
});
This example omits the actual networking portion of the system, and instead simply calls functions on the target object directly, and replies immediately. In a real-world example, the function request and its parameters would be serialized, along with a unique identifier specifying which object to call, passed across a network or across threads, decoded and run on the destination process, and then likewise the reply serialized and returned. One could adapt the system to be either synchronous, where the caller waits until the reply or failure has been received, or asynchronous where reply callbacks are processed at regular intervals. For example, imagine inserting a simple .async
into the call chain, specifying timeouts, or setting other options. But otherwise, this program should fully compile and give an idea of how such a system might work.
import std.stdio;
import std.format;
import std.traits;
import std.exception;
import util.extratraits;
class Person {
mixin ProxyReceiver;
string name;
int age;
this(string name, int age) {
this.name = name;
this.age = age;
}
@Proxy
@Reply!(string)
void requestName() {
writefln("SERVER# User requested name");
reply(name);
}
@Proxy
@Reply!(bool)
void setName(string s) {
writefln("SERVER# Changed name to: %s", s);
this.name = s;
reply(true);
}
@Proxy
@Reply!(int)
@Failure!(string)
void doubleEvenNumber(int x) {
writefln("SERVER# User wants to double number: %s", x);
if (!(x % 2)) reply(x * 2, x);
else failure("Supplied number is not even.");
}
}
struct Reply(RA...) {}
struct Failure(RA...) {}
mixin template ProxyReceiver() {
Proxy!(typeof(this)) _proxy;
void reply(string funcstr = __FUNCTION__, RA...)(RA rargs) {
_proxy.reply!(funcstr)(rargs);
}
void failure(string funcstr = __FUNCTION__, RA...)(RA rargs) {
_proxy.failure!(funcstr)(rargs);
}
}
class Proxy(Destination) {
Destination dest;
private static abstract class Callback {
MsgNum msgNum;
double startTime, timeout;
}
private static final class CallbackT(alias FUNC) : Callback {
static if (hasUDA!(FUNC, Reply)) {
alias SUCCESS = void delegate(TemplateArgsOf!(getUDAs!(FUNC, Reply)[0]));
SUCCESS successDG;
}
static if (hasUDA!(FUNC, Failure)) {
alias FAILURE = void delegate(TemplateArgsOf!(getUDAs!(FUNC, Failure)[0]));
FAILURE failureDG;
}
}
alias MsgNum = uint;
MsgNum nextMsgNum = 1;
Callback[MsgNum] callbacks;
MsgNum lastMessageNum;
alias FIRE = void delegate();
FIRE[MsgNum] pendingCalls; // use delegate here as simulation for networking delay to allow time to add callbacks
this(Destination dest) {
this.dest = dest;
dest._proxy = this;
}
void reply(string funcstr = __FUNCTION__, RA...)(RA rargs) {
mixin(`alias FUNC = `~funcstr~`;`);
alias FQN = fullyQualifiedName!FUNC;
static assert(hasUDA!(FUNC, Reply), "No reply allowed for func: "~FQN);
alias UDA = getUDAs!(FUNC, Reply)[0];
alias RFUNC = void delegate(TemplateArgsOf!UDA);
static assert(canCallFuncWithParameters!(RFUNC, RA), format("Invalid parameters for reply: %s (expected %s)", RA.stringof, Parameters!RFUNC.stringof));
auto msgNum = lastMessageNum;
auto p = msgNum in callbacks;
enforce(p, format("Callback not found: #%s", msgNum));
auto cbase = *p;
callbacks.remove(msgNum);
auto cb = cast(CallbackT!FUNC) cbase;
enforce(cb, "Callback mismatch: could not cast to %s", CallbackT!FUNC.stringof);
//writefln("Ready to call cb(%s) delegate with args: %s", cb, rargs);
if (cb.successDG !is null) {
cb.successDG(rargs);
cb.successDG = null;
}
}
void failure(string funcstr = __FUNCTION__, RA...)(RA rargs) {
mixin(`alias FUNC = `~funcstr~`;`);
alias FQN = fullyQualifiedName!FUNC;
static assert(hasUDA!(FUNC, Failure), "No failure allowed for func: "~FQN);
alias UDA = getUDAs!(FUNC, Failure)[0];
alias RFUNC = void delegate(TemplateArgsOf!UDA);
static assert(canCallFuncWithParameters!(RFUNC, RA), format("Invalid parameters for failure: %s (expected %s)", RA.stringof, Parameters!RFUNC.stringof));
auto msgNum = lastMessageNum;
auto p = msgNum in callbacks;
enforce(p, format("Callback not found: #%s", msgNum));
auto cbase = *p;
callbacks.remove(msgNum);
auto cb = cast(CallbackT!FUNC) cbase;
enforce(cb, "Callback mismatch: could not cast to %s", CallbackT!FUNC.stringof);
if (cb.failureDG !is null) {
cb.failureDG(rargs);
cb.failureDG = null;
}
}
struct Outbound(alias FUNC) {
this() @disable;
this(this) @disable;
~this() {
//writefln("~Outbound!%s [%s:%s]", FUNCNAME!FUNC, msgNum, fired);
if (!fired)
go();
}
alias FQN = fullyQualifiedName!FUNC;
static if (hasUDA!(FUNC, Reply)) {
alias SUCCESS = void delegate(TemplateArgsOf!(getUDAs!(FUNC, Reply)[0]));
}
static if (hasUDA!(FUNC, Failure)) {
alias FAILURE = void delegate(TemplateArgsOf!(getUDAs!(FUNC, Failure)[0]));
}
private Proxy proxy;
private MsgNum msgNum;
private bool fired;
private this(Proxy proxy, MsgNum n) {
this.proxy = proxy;
this.msgNum = n;
}
static if (is(SUCCESS))
auto onSuccess(SUCCESS dg) scope return {
static assert(is(SUCCESS), ("Reply not allowed for %s", FQN));
CallbackT!FUNC cb;
if (auto p = msgNum in proxy.callbacks) {
cb = cast(CallbackT!FUNC) *p;
assert(cb, "Callback type mismatch");
assert(cb.successDG is null, "Success callback already set");
} else {
cb = new CallbackT!FUNC;
cb.msgNum = msgNum;
proxy.callbacks[cb.msgNum] = cb;
}
cb.successDG = dg;
fired = true; // Returning new struct so this is now defunct
return Outbound(proxy, msgNum);
}
static if (is(FAILURE))
auto onFailure(FAILURE dg) scope return {
static assert(is(FAILURE), ("Failure not allowed for %s", FQN));
CallbackT!FUNC cb;
if (auto p = msgNum in proxy.callbacks) {
cb = cast(CallbackT!FUNC) *p;
assert(cb, "Callback type mismatch");
assert(cb.failureDG is null, "Failure callback already set");
} else {
cb = new CallbackT!FUNC;
cb.msgNum = msgNum;
proxy.callbacks[cb.msgNum] = cb;
}
cb.failureDG = dg;
fired = true; // Returning new struct so this is now defunct
return Outbound(proxy, msgNum);
}
void go() {
if (fired) return;
fired = true;
if (auto fireDG = msgNum in proxy.pendingCalls) {
proxy.pendingCalls.remove(msgNum);
(*fireDG)();
}
}
}
auto opDispatch(string s, SA...)(SA sargs) {
//writefln("opDispatch: <%s>%s", s, SA.stringof);
alias FUNC = __traits(getMember, dest, s);
alias FN = FUNCNAME!FUNC;
alias FQN = fullyQualifiedName!FUNC;
static if (!hasTemplateUDA!(FUNC, Proxy)) {
pragma(msg, format("Cannot call function %s without @Proxy UDA", FQN)); // Difficult to get compilation error messages inside opDispatch
static assert(false);
}
alias PARAMS = Parameters!FUNC;
static if (!canCallFuncWithParameters!(FUNC, SA)) {
pragma(msg, format("Invalid parameters for proxy %s: expected %s, got %s", FQN, PARAMS.stringof, SA.stringof));
static assert(false, "Invalid parameters");
} else {
auto msgNum = nextMsgNum++;
auto outbound = Outbound!FUNC(this, msgNum);
pendingCalls[msgNum] = {
// This delegate calls the receiver object directly. In reality, we would
// split Proxy into a Transmitter/Receiver pair and serialize a message
// across the network to be reconstructed and remotely call the receiver
// on the destination object.
lastMessageNum = msgNum;
__traits(getMember, dest, s)(sargs);
};
return outbound;
}
}
}
void main() {
auto person = new Person("bob", 34);
auto proxy = new Proxy!Person(person);
proxy.requestName().onSuccess((string str) {
writefln("Client| Received name: %s", str);
});
proxy.doubleEvenNumber(4).onSuccess((int r, int orig) {
writefln("Client| Received doubled number: %s (Original number was: %s)", r, orig);
}).onFailure((string str) {
writefln("Client| Error: %s", str);
});
proxy.doubleEvenNumber(3).onSuccess((int r, int orig) {
writefln("Client| Received doubled number: %s (Original number was: %s)", r, orig);
}).onFailure((string str) {
writefln("Client| Error: %s", str);
});
assert(proxy.callbacks.length == 0, format("Unhandled callbacks: %s", proxy.callbacks));
}
.
My util/extratraits.d
for some additional helper templates:
module util.extratraits;
import std.traits;
template FUNCNAME(F...) if (F.length == 1) {
import std.string;
enum FUNCNAME = fullyQualifiedName!(F[0])[ fullyQualifiedName!(F[0]).lastIndexOf('.')+1 .. $ ];
}
bool canCallFuncWithParameters(alias FUNC, SA...)() pure @safe nothrow {
static if (SA.length > Parameters!FUNC.length || SA.length < numRequiredArguments!FUNC) {
return false;
}
static foreach (idx, P; Parameters!FUNC) {
static if (idx < SA.length && !isImplicitlyConvertible!(SA[idx], P)) {
return false;
}
}
return true;
}
size_t numRequiredArguments(alias FUNC)() pure @safe nothrow {
size_t numReq = 0;
static foreach (argi, PD; ParameterDefaults!FUNC) {
static if (is(PD == void))
numReq++;
else
return numReq;
}
return numReq;
}
string shortname()(string str) {
import std.string;
auto excl = str.indexOf('!');
if (excl > 0) str = str[0 .. excl];
return str[str.lastIndexOf('.')+1 .. $];
}
bool hasTemplateUDA(alias SYM, alias UDA)() {
static foreach (idx, attr; __traits(getAttributes, SYM)) {
static if (__traits(isTemplate, TemplateOf!UDA)) {
static if (__traits(isSame, attr, TemplateOf!UDA))
return true;
} else {
static if (__traits(isSame, attr, UDA))
return true;
}
}
return false;
}
template isMemberVariable(alias THIS, alias SYM) if (isAggregateType!THIS) {
enum bool isMemberVariable = !(isFunction!SYM || isType!SYM || __traits(isTemplate, SYM) /*|| hasStaticMember!(THIS, SYM.stringof)*/);
}