Thread overview
D Programming: How to Define and Use Custom Attributes
Sep 06, 2023
Soham Mukherjee
Sep 06, 2023
bachmeier
Sep 06, 2023
H. S. Teoh
Sep 08, 2023
Nicholas Wilson
Sep 10, 2023
cc
Sep 10, 2023
cc
Sep 13, 2023
Gulshan Negi
Sep 13, 2023
Basile B.
September 06, 2023

I'm exploring the D programming language and I'm curious about custom attributes. How can I define and use custom attributes in D to add metadata or behavior to my code?

For example, let's say I want to create a custom attribute @MyCustomAttribute that I can apply to functions or types. How do I define this attribute, and how can I use it to influence the behavior of my D code?

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!

September 06, 2023

On Wednesday, 6 September 2023 at 18:54:26 UTC, Soham Mukherjee wrote:

>

I'm exploring the D programming language and I'm curious about custom attributes. How can I define and use custom attributes in D to add metadata or behavior to my code?

For example, let's say I want to create a custom attribute @MyCustomAttribute that I can apply to functions or types. How do I define this attribute, and how can I use it to influence the behavior of my D code?

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!

Ali's book has a chapter on UDAs:

http://ddili.org/ders/d.en/uda.html

September 06, 2023
On Wed, Sep 06, 2023 at 06:54:26PM +0000, Soham Mukherjee via Digitalmars-d wrote:
> I'm exploring the D programming language and I'm curious about custom attributes. How can I define and use custom attributes in D to add metadata or behavior to my code?
> 
> For example, let's say I want to create a custom attribute @MyCustomAttribute that I can apply to functions or types. How do I define this attribute, and how can I use it to influence the behavior of my D code?
> 
> 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!

User-defined attributes (UDAs) in D are primarily used for compile-time introspection.  It lets you tag declarations with compile-time metadata that you can then inspect from other code at compile-time.

One example is command-line option parsing code that prints out automatically-generated help text when the program is passed a `--help` option.  The idea is to encapsulate your command-line options in a struct, for example:

	struct Options
	{
		 double xmin;
		 double xmax;
		 double ymin;
		 double ymax;

		 int xres;
		 int yres;
	}

You could use compile-time introspection to iterate over struct members and use field names as option names (e.g., so you could run the program with `--xmin=1.0 --ymax=10.0`, etc.). You can do this without any UDAs, but suppose you want to go one step further, and have `--help` automatically print out all available options. You could manually write a showHelp() function that prints this information as a big string, of course, but that has the disadvantage that the help text is detached from the definition of the Options struct. If you subsequently modify the Options struct, your help text would become out-of-date and contain possiubly misleading or wrong information.  It would be better if the help text could be embedded directly in the definition of Options, so that `--help` will *always* be up-to-date. The way you do this is to add UDAs to it:

	struct Desc { string text; }

	struct Options
	{
		 @Desc("Minimum X-coordinate") double xmin;
		 @Desc("Maximum X-coordinate") double xmax;
		 @Desc("Minimum Y-coordinate") double ymin;
		 @Desc("Maximum Y-coordinate") double ymax;

		 @Desc("Output horizontal resolution") int xres;
		 @Desc("Output vertical resolution") int yres;
	}

By themselves, these UDAs don't do anything. But now you can inspect them in your showHelp() function to print out the UDA strings as part of its output:

	void showHelp() {
		Options opts;
		writefln("Usage: myprogram [options]");
		writeln("Options:");
		foreach (field; __traits(allMembers(Options))) {
			alias desc = getUDAs!(mixin("opts."~field), Desc);
			writefln("--%s", field);
			if (desc.length > 0)
				writefln("\t%s", desc);
		}
	}

Now showHelp() will print out the description for each field in Options, and it will always display the correct information, as long as you update the UDA in Options whenever you change the definition of a field.

You don't have to stop here.  You can go even further, and have automatic option range enforcement. For example, if Options.xres must be between 10 and 5000, you could write code explicitly to check that, but again, the check will be detached from the definition of Options so it's liable to go out-of-sync.  But with UDAs, we can add this information to the definition of Options, and have the option-parsing code automatically enforce this:

	struct Desc { string text; }

	struct Range { int minVal, maxVal; }

	struct Options
	{
		 @Desc("Minimum X-coordinate") double xmin;
		 @Desc("Maximum X-coordinate") double xmax;
		 @Desc("Minimum Y-coordinate") double ymin;
		 @Desc("Maximum Y-coordinate") double ymax;

		 @Range(10, 5000) @Desc("Output horizontal resolution") int xres;
		 @Range(8, 4000) @Desc("Output vertical resolution") int yres;
	}

Then in your option-parsing code:

	void parseOption(ref Options opts, string name, string value) {
		foreach (field; __traits(allMembers(Options))) {
			if (name == field) {
				import std.conv : to;
				alias T = typeof(mixin("opts."~field));
				T val = value.to!T; // parse option value

				// Automatic enforcement of option value
				// range
				alias range = getUDAs!(mixin("opts."~field), Range);
				if (range.length > 0) {
					enforce(val >= range.minVal,
						"value of "~field~" is below minimum");
					enforce(val <= range.maxVal,
						"value of "~field~" is above maximum");
				}

				// Range check passed (or no Range UDA
				// was found), assign parsed value to
				// options struct.
				mixin("opts."~field) = val;
			}
		}
	}

Now all you have to do is to update the Range UDA in Options, and parseOption will automatically enforce the new range. A code change in one place, and all other code that inspects the UDA will update themselves automatically.

You can also use UDAs to mark options that require special processing. For example, say int fields in Options are normally just converted from base 10, but one particular field, say `octalvalue`, expects user input in octal instead. Instead of writing if-then-else spaghetti in parseOption for each such exception, create a new UDA, say Octal, that you attach to the definition of `octalvalue`. Then in parseOption(), check the field for this UDA, and if it's present, parse the value using base 8 instead of base 10.

This may seem like a lot of work for something you could do with just a single if-statement, but what if in the future you need another field that's in octal?  With the UDA, you can just tack it on in the definition of Options, and everything will Just Work(tm).  Glancing at the definition of Options will immediately tell you that this field is parsed in Octal; no need to dig through the code of parseOption() to find out about this. The UDA serves as self-documentation, which is a good thing.


There are many other examples of UDA usage that I'm sure others will be able to provide.


T

-- 
The irony is that Bill Gates claims to be making a stable operating system and Linus Torvalds claims to be trying to take over the world. -- Anonymous
September 08, 2023

On Wednesday, 6 September 2023 at 18:54:26 UTC, Soham Mukherjee wrote:

>

I'm exploring the D programming language and I'm curious about custom attributes. How can I define and use custom attributes in D to add metadata or behavior to my code?

For example, let's say I want to create a custom attribute @MyCustomAttribute that I can apply to functions or types. How do I define this attribute, and how can I use it to influence the behavior of my D code?

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!

for an example of what the can be used for, I use them for attaching metadata to structs describing the values used to query them from an API and the use that in templates to generate code that to return those values

https://github.com/libmir/dcompute/blob/master/source/dcompute/driver/ocl/context.d#L20
https://github.com/libmir/dcompute/blob/master/source/dcompute/driver/ocl/util.d#L117

September 10, 2023

On Wednesday, 6 September 2023 at 18:54:26 UTC, Soham Mukherjee wrote:

>

I'm exploring the D programming language and I'm curious about custom attributes. How can I define and use custom attributes in D to add metadata or behavior to my code?

UDAs can be extremely useful for iterating through select members of a type when reading data from an input source, such as XML or JSON data. A very basic setup might look something like this (very stripped example written from memory, lot of blanks to be filled in):

struct XMLField {} // We'll use an empty struct for our UDA rather than enum since it's
                   // guaranteed to be a globally unique symbol

class Person {
	@XMLField { // Everything in this block gets the @XMLField UDA
		string name;
		int age;
	}
}

auto data = `
<People>
	<Person>
		<name>Bob</name>
		<age>34</age>
	</Person>
	<Person>
		<name>Joe</name>
		<age>20</age>
	</Person>
</People>
`;

auto xml = MyXMLParser(data); // supply your own xml parser
foreach (node; xml.find("Person")) {
	auto p = new Person;
	readFromXML(p, node);
}

void readFromXML(T)(T t, XMLNode node) {
	foreach (idx, field; T.tupleof) { // iterate through all member variables on class
		static if (hasUDA!(T.tupleof[idx], XMLField)) { // If it has the XMLField UDA...
			enum NAME = field.stringof; // the name of the member (name, age, etc)
			alias TYPE = typeof(field);
			if (auto text = node.find(NAME).text) { // find child XML node that matches member
				try {
					t.tupleof[idx] = text.to!TYPE; // std.conv.to to convert text to the appropriate type
				} catch (ConvException e) {
					...
				}
			}
		}
	}
}

An ultimate goal is often to use introspection to minimize the number of places, in code, that need to be "touched" every time a data structure is modified or new fields added. In an older, more straightforward program, if I added a new field to Person, I would also need to remember to change the files responsible for reading that field from the input data, validating it, etc. Here, I only need to add the new field with the UDA to the class, and the rest is taken care of (the UDA, itself, is not strictly necessary here, but it helps to let the system know which fields to process and which to ignore!). Of course, in a real-world program, significantly more care would need to be taken to validate the data, handle errors, conversions, missing entries, duplicate entries, and so on. But once your system is solid, it minimizes the pain of upgrading the important stuff.

Some other richer ideas include:

  • Attributing classes to specify how to dynamically bind to LUA
  • Configuration settings that restrict allowable values to be stored from a text command console
  • Marking functions to be available via an RPC library and how to mangle them
September 10, 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.

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)*/);
}
September 13, 2023

Well, In D we can define Custom Attributes in very simple ways, lets understand it with the help of below example.

module mycustomattribute.d;

public struct MyCustomAttribute {
string data;

this(string data) {
    this.data = data;
}

}

Apart from this we can use Custom Attributes to add additional in out code.

Thanks

September 13, 2023

On Wednesday, 6 September 2023 at 18:54:26 UTC, Soham Mukherjee wrote:

>

I'm exploring the D programming language and I'm curious about custom attributes. How can I define and use custom attributes in D to add metadata or behavior to my code?

For example, let's say I want to create a custom attribute @MyCustomAttribute that I can apply to functions or types. How do I define this attribute, and how can I use it to influence the behavior of my D code?

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!

I used UDA to

A very simple example written recently (previous are rather old): class qualifiers, allowing to restrict the candidate in an overload set, in a fashion that ressemble a recent c++ feature.