D Programming: How to Define and Use Custom Attributes

cc cc at nevernet.com
Sun Sep 10 20:13:17 UTC 2023


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.

```d
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.

```d
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:
```d
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)*/);
}
```


More information about the Digitalmars-d mailing list