1 /// Implements a C#-style multicast delegate which is simply a collection of
2 /// delegates acting like one single delegate.
3 ///
4 /// All functionality is in the type $(LREF MulticastImpl) with a helper
5 /// construction function called $(LREF multicast).
6 ///
7 /// Use $(LREF Multicast) for an API which asserts out if you attempt to call
8 /// uninitialized Multicast values or $(LREF MulticastOpt) for an API which
9 /// simply does nothing when called with uninitialized values.
10 ///
11 /// Authors: webfreak
12 /// License: released in the public domain under the unlicense, see LICENSE
13 module multicast_delegate;
14 
15 // version = CSharpCompat;
16 
17 import std.algorithm : remove;
18 import std.functional : forward;
19 import std.traits : FunctionAttribute, functionAttributes,
20 	functionLinkage, isDelegate, isFunctionPointer,
21 	Parameters, ReturnType, SetFunctionAttributes, Unqual;
22 
23 @safe:
24 
25 static if (is(size_t == ulong))
26 	private enum needsCopyBit = 1UL << 63UL;
27 else
28 	private enum needsCopyBit = cast(size_t)(
29 				1 << (8 * size_t.sizeof - 1));
30 
31 version (unittest)
32 {
33 	private int dupCount;
34 }
35 
36 /// Constructs a multicast delegate from a variadic list of delegates.
37 Multicast!Del multicast(Del)(Del[] delegates...)
38 		if (isDelegate!Del || isFunctionPointer!Del)
39 {
40 	return Multicast!Del(delegates);
41 }
42 
43 /// MulticastDelegate API which asserts out if you attempt to call it with an
44 /// empty list.
45 alias Multicast(Del) = MulticastImpl!(Del, true);
46 
47 /// MulticastDelegate API which simply does nothing if you attempt to call it
48 /// with an empty list. Returns the init value and performs no out/ref
49 /// modifications.
50 alias MulticastOpt(Del) = MulticastImpl!(Del, false);
51 
52 /// Full multicast delegate implementation of this package. Modification
53 /// operations are nothrow, pure, @safe and if possible @nogc. Using the `@nogc`
54 /// overloads it's possible to use all functionality. Invocation inherits the
55 /// attributes of the delegate or function pointer.
56 ///
57 /// Even though the multicast implementation acts like a reference type, it is
58 /// actually a value type and not a reference type, so modifications of copies
59 /// will not be reflected in the source object. While normal copying by passing
60 /// around the multicast delegate by value doesn't copy the underlying array
61 /// data, this data structure will track copies and perform a copy on write
62 /// whenever state is changed in a way that would affect other instances. Relies
63 /// on GC to cleanup duplicated arrays.
64 ///
65 /// When a multicast delegate is invoked (converted to a normal delegate and
66 /// invoked), all methods are called in order. Reference parameters will be
67 /// forwarded and passed sequentially to each method. Any changes in reference
68 /// parameters are visible to the next method. When any of the methods throws an
69 /// exception that is not caught within the method, that exception is passed to
70 /// the caller of the delegate and no subsequent methods in the invocation list
71 /// are called. If the delegate has a return value and/or out parameters, it
72 /// returns the return value and parameters of the last method invoked.
73 ///
74 /// Function attributes for delegates and function pointers are inherited to the
75 /// simulated delegate. Function pointers are changed to being delegates however
76 /// and the linkage of the delegate will be `extern(D)` as other linkage doesn't
77 /// make sense for D delegates.
78 ///
79 /// This is a lot like the MulticastDelegate type in C#, which performs the same
80 /// operations. See$(BR)
81 /// $(LINK https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/delegates/using-delegates)$(BR)
82 /// $(LINK https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/delegates/how-to-combine-delegates-multicast-delegates)$(BR)
83 /// $(LINK https://docs.microsoft.com/en-us/dotnet/api/system.multicastdelegate?view=netcore-3.1)$(BR)
84 ///
85 /// There are optional C#-style method overloads for + and - as well as an added
86 /// GetInvocationList method which simply returns the delegates. To use these,
87 /// compile the library with `version = CSharpCompat;`.
88 ///
89 /// Params:
90 ///    Del = the delegate or function pointer type which this MulticastImpl
91 ///          simulates. In case of function pointers this will be a delegate of
92 ///          the same return value, parameters and attributes.
93 ///
94 ///    assertIfNull = if true, crash with assert(false) in case you try to call
95 ///          an instance with no delegates set.
96 struct MulticastImpl(Del, bool assertIfNull)
97 		if (isDelegate!Del || isFunctionPointer!Del)
98 {
99 	union
100 	{
101 		/// The list of delegates or function pointers. This is an internal
102 		/// field to the struct which may be tainted with an extra bit,
103 		/// corrupting any attempted use or modification. Use the
104 		/// $(LREF delegates) property to get or set the list of delegates
105 		/// instead. Use `add` and `remove` to perform efficient adding and
106 		/// removing which takes care of memory ownership.
107 		Del[] _delegates;
108 
109 		/// Magic storage of a copy bit which is set when a postblit occurs to
110 		/// signal that mutating functions (except add) must copy the delegates
111 		/// array before modification.
112 		size_t _accessMask;
113 	}
114 
115 	/// Constructs this multicast delegate without any value, same as init.
116 	this(typeof(null)) nothrow pure @nogc immutable
117 	{
118 	}
119 
120 	/// Constructs this multicast delegate with a singular delegate to call.
121 	this(Del one) nothrow pure immutable @trusted
122 	{
123 		_delegates = [one];
124 	}
125 
126 	/// Constructs this multicast delegate with a given array of delegates. Does
127 	/// not duplicate the array, so future modifications might change the
128 	/// behavior of this delegate. It is recommended only to use this for @nogc
129 	/// compatibility.
130 	this(immutable(Del)[] all) nothrow pure @nogc immutable @trusted
131 	{
132 		_delegates = all;
133 		(cast() this)._accessMask |= needsCopyBit;
134 	}
135 
136 	/// ditto
137 	this(Del[] all) nothrow pure @nogc @trusted
138 	{
139 		_delegates = all;
140 	}
141 
142 	this(this)
143 	{
144 		_accessMask |= needsCopyBit;
145 	}
146 
147 	/// Returns the _delegates member with the magic access mask storage
148 	/// stripped away for normal usage.
149 	inout(Del[]) delegates() inout nothrow pure @nogc @trusted
150 	{
151 		const mask = _accessMask;
152 		cast() _accessMask &= ~needsCopyBit;
153 		inout(Del[]) ret = _delegates;
154 		cast() _accessMask = mask;
155 		return ret;
156 	}
157 
158 	/// Sets the _delegates member and resets the magic copy bit.
159 	ref auto delegates(Del[] all) nothrow pure @nogc @trusted
160 	{
161 		_accessMask &= ~needsCopyBit;
162 		_delegates = all;
163 		return this;
164 	}
165 
166 	/// Adds one or more delegates to this multicast delegate in order. Returns
167 	/// a reference to this instance.
168 	ref auto add(const Del[] dels) nothrow pure @trusted
169 	{
170 		const copyBit = (_accessMask & needsCopyBit) != 0;
171 		_accessMask &= ~needsCopyBit;
172 		const ptr = _delegates.ptr;
173 		scope (exit)
174 			if (copyBit && _delegates.ptr == ptr)
175 				_accessMask |= needsCopyBit;
176 		_delegates ~= dels;
177 		return this;
178 	}
179 
180 	/// ditto
181 	ref auto add(bool v)(const MulticastImpl!(Del, v) del) nothrow pure @trusted
182 	{
183 		const copyBit = (_accessMask & needsCopyBit) != 0;
184 		_accessMask &= ~needsCopyBit;
185 		const ptr = _delegates.ptr;
186 		scope (exit)
187 			if (copyBit && _delegates.ptr == ptr)
188 				_accessMask |= needsCopyBit;
189 		_delegates ~= del.delegates;
190 		return this;
191 	}
192 
193 	/// ditto
194 	ref auto add(const Del del) nothrow pure @trusted
195 	{
196 		const copyBit = (_accessMask & needsCopyBit) != 0;
197 		_accessMask &= ~needsCopyBit;
198 		const ptr = _delegates.ptr;
199 		scope (exit)
200 			if (copyBit && _delegates.ptr == ptr)
201 				_accessMask |= needsCopyBit;
202 		_delegates ~= del;
203 		return this;
204 	}
205 
206 	/// Removes a delegate from this multicast delegate. Returns a reference to
207 	/// this instance. Note that this will duplicate the array in case this
208 	/// delegate did not create it.
209 	ref auto remove(const Del del) nothrow pure @trusted
210 	{
211 		if ((_accessMask & needsCopyBit) != 0)
212 		{
213 			_accessMask &= ~needsCopyBit;
214 			_delegates = _delegates.dup.remove!(
215 					a => a == del);
216 
217 			version (unittest)
218 				debug dupCount++;
219 		}
220 		else
221 		{
222 			_delegates = _delegates.remove!(a => a == del);
223 		}
224 
225 		return this;
226 	}
227 
228 	/// Removes a delegate from this multicast delegate, assumes this instance
229 	/// holds a unique reference to the delegates array. If this is called on
230 	/// copies of this multicast instance, change in behavior may occur.
231 	ref auto removeAssumeUnique(const Del del) nothrow pure @nogc @trusted
232 	{
233 		_delegates = _delegates.remove!(a => a == del);
234 		return this;
235 	}
236 
237 	/// Reassigns this multicast delegate to a single delegate. Returns a
238 	/// reference to this instance.
239 	ref auto opAssign(const Del one) nothrow pure
240 	{
241 		delegates = [one];
242 		return this;
243 	}
244 
245 	/// Reassigns this multicast delegate with a given array of delegates. Does
246 	/// not duplicate the array, so future modifications might change the
247 	/// behavior of this delegate. It is recommended only to use this for @nogc
248 	/// compatibility. Returns a reference to this instance.
249 	ref auto opAssign(Del[] all) nothrow pure @nogc
250 	{
251 		delegates = all;
252 		return this;
253 	}
254 
255 	/// Unsets this multicast delegate to the init state.
256 	ref auto opAssign(typeof(null)) nothrow pure @nogc @trusted
257 	{
258 		_accessMask &= ~needsCopyBit;
259 		_delegates = null;
260 		_accessMask = 0;
261 		return this;
262 	}
263 
264 	/// Overloads `~=` operator to call add.
265 	alias opOpAssign(string op : "~") = add;
266 
267 	/// Overloads any binary operator (+, -, ~) to operate on a copy of this
268 	/// multicast delegate. Performs a duplication of the delegates array using
269 	/// the GC.
270 	auto opBinary(string op, T)(const T rhs) const
271 	{
272 		Unqual!(typeof(this)) copy;
273 		copy.delegates = delegates.dup;
274 		version (unittest)
275 			debug dupCount++;
276 		copy.opOpAssign!op(rhs);
277 		return copy;
278 	}
279 
280 	/// Checks if there are any delegates and if this can be called.
281 	bool opCast(T : bool)() const nothrow pure @nogc @trusted
282 	{
283 		return _delegates.length > 0;
284 	}
285 
286 	/// Implementation: actually calls the delegates for this multicast delegate.
287 	/// This has no function attributes set on it, so it's not recommended to be
288 	/// called manually.
289 	ReturnType!Del _invokeImpl(Parameters!Del params) const @trusted
290 	{
291 		const copyBit = (_accessMask & needsCopyBit) != 0;
292 		cast() _accessMask &= ~needsCopyBit;
293 		scope (exit)
294 			if (copyBit)
295 				cast() _accessMask |= needsCopyBit;
296 
297 		if (!delegates.length)
298 		{
299 			static if (assertIfNull)
300 				assert(false, "Tried to call unassigned multicast delegate");
301 			else static if (is(typeof(return) == void))
302 				return;
303 			else
304 				return typeof(return).init;
305 		}
306 
307 		foreach (Del del; _delegates[0 .. $ - 1])
308 			del(forward!params);
309 		return _delegates[$ - 1](forward!params);
310 	}
311 
312 	/// Implementation: takes $(LREF _invokeImpl) as a delegate and adds the
313 	/// function attributes of the wrapping delegate. This may be considered an
314 	/// unsafe operation, especially with future added attributes which could
315 	/// change the behavior of a function call. The ABI is that of a normal
316 	/// `extern(D) T delegate(Args...) const @trusted` which gets changed to all
317 	/// the attributes of the wrapping delegate.
318 	auto _invokePtr() const nothrow pure @nogc @trusted
319 	{
320 		return cast(SetFunctionAttributes!(typeof(&_invokeImpl),
321 				"D", functionAttributes!Del))&_invokeImpl;
322 	}
323 
324 	/// Converts this multicast delegate to a normal delegate. This is also used
325 	/// as `alias this`, so this multicast delegate can be passed as delegate
326 	/// argument to functions or be called directly.
327 	alias toDelegate = _invokePtr;
328 
329 	/// ditto
330 	alias toDelegate this;
331 
332 	version (CSharpCompat)
333 	{
334 		/// Adds `+=` operator support for C# code compatibility
335 		alias opOpAssign(string op : "+") = add;
336 
337 		/// Adds `-=` operator support for C# code compatibility
338 		alias opOpAssign(string op : "-") = remove;
339 
340 		/// Returns the delegates of this multicast.
341 		auto GetInvocationList() const nothrow pure @nogc
342 		{
343 			return delegates;
344 		}
345 	}
346 }
347 
348 ///
349 @system unittest
350 {
351 	int modify1(ref string[] stack)
352 	{
353 		stack ~= "1";
354 		return cast(int) stack.length;
355 	}
356 
357 	int modify2(ref string[] stack)
358 	{
359 		stack ~= "2";
360 		return 9001;
361 	}
362 
363 	string[] stack;
364 
365 	// del is like a delegate now
366 	Multicast!(int delegate(ref string[])) del = &modify1;
367 	assert(del(stack) == 1);
368 	assert(stack == ["1"]);
369 
370 	stack = null;
371 	del ~= &modify2;
372 	assert(del(stack) == 9001);
373 	assert(stack == ["1", "2"]);
374 
375 	void someMethod(int delegate(ref string[]) fn)
376 	{
377 	}
378 
379 	someMethod(del);
380 	someMethod(del);
381 }
382 
383 @safe unittest
384 {
385 	import std.exception;
386 	import core.exception;
387 
388 	string[] calls;
389 
390 	void call1()
391 	{
392 		calls ~= "1";
393 	}
394 
395 	void call2()
396 	{
397 		calls ~= "2";
398 	}
399 
400 	Multicast!(void delegate() @safe) del;
401 	MulticastOpt!(void delegate() @safe) delOpt;
402 
403 	(() @trusted => assertThrown!AssertError(del()))();
404 	delOpt();
405 
406 	del = &call1;
407 	del();
408 	assert(calls == ["1"]);
409 
410 	calls = null;
411 	delOpt = &call1;
412 	delOpt();
413 	assert(calls == ["1"]);
414 
415 	calls = null;
416 	del();
417 	assert(calls == ["1"]);
418 
419 	calls = null;
420 	del();
421 	del();
422 	assert(calls == ["1", "1"]);
423 
424 	calls = null;
425 	del ~= &call1;
426 	del();
427 	assert(calls == ["1", "1"]);
428 
429 	calls = null;
430 	del ~= &call2;
431 	del();
432 	assert(calls == ["1", "1", "2"]);
433 }
434 
435 @safe unittest
436 {
437 	alias Del = int delegate(long) @safe nothrow @nogc pure;
438 
439 	int fun1(long n) @safe nothrow @nogc pure
440 	{
441 		return cast(int)(n - 1);
442 	}
443 
444 	int fun2(long n) @safe nothrow @nogc pure
445 	{
446 		return cast(int)(n - 2);
447 	}
448 
449 	Multicast!Del foo = [&fun1, &fun2];
450 
451 	assert((() nothrow @nogc pure => foo(8))() == 6);
452 
453 	void someMethod(Del fn)
454 	{
455 		fn(4);
456 	}
457 
458 	someMethod(foo);
459 }
460 
461 @safe unittest
462 {
463 	import std.exception;
464 
465 	alias Del = void delegate() @safe;
466 
467 	int[] stack;
468 
469 	void f1()
470 	{
471 		stack ~= 1;
472 	}
473 
474 	void f2()
475 	{
476 		stack ~= 2;
477 		throw new Exception("something occurred");
478 	}
479 
480 	void f3()
481 	{
482 		stack ~= 3;
483 	}
484 
485 	Multicast!Del something;
486 	something ~= &f1;
487 	something ~= &f2;
488 	something ~= &f3;
489 
490 	assertThrown(something());
491 	assert(stack == [1, 2]);
492 }
493 
494 @safe unittest
495 {
496 	alias Del = void function(ref int[]) @safe;
497 
498 	int[] stack;
499 
500 	static void f1(ref int[] stack) @safe
501 	{
502 		stack ~= 1;
503 	}
504 
505 	static void f2(ref int[] stack) @safe
506 	{
507 		stack ~= 2;
508 	}
509 
510 	Multicast!Del something;
511 	something ~= &f1;
512 	something ~= &f2;
513 
514 	something(stack);
515 	assert(stack == [1, 2]);
516 
517 	stack = null;
518 	(something ~ &f2)(stack);
519 	assert(stack == [1, 2, 2]);
520 
521 	stack = null;
522 	const Multicast!Del constSomething = &f1;
523 
524 	constSomething(stack);
525 	assert(stack == [1]);
526 
527 	stack = null;
528 	(constSomething ~ &f2)(stack);
529 	assert(stack == [1, 2]);
530 
531 	stack = null;
532 	immutable Multicast!Del immutableSomething = &f1;
533 	immutable Multicast!Del immutableSomething2 = [
534 		&f1, &f2
535 	];
536 
537 	immutableSomething(stack);
538 	assert(stack == [1]);
539 
540 	stack = null;
541 	(immutableSomething ~ &f2)(stack);
542 	assert(stack == [1, 2]);
543 }
544 
545 @safe unittest
546 {
547 	alias Del = void function(ref int[]) @safe;
548 
549 	int[] stack;
550 
551 	static void f1(ref int[] stack) @safe
552 	{
553 		stack ~= 1;
554 	}
555 
556 	static void f2(ref int[] stack) @safe
557 	{
558 		stack ~= 2;
559 	}
560 
561 	Multicast!Del something;
562 	something ~= &f1;
563 	something ~= &f2;
564 
565 	Multicast!Del copy = something;
566 	copy.remove(&f1);
567 
568 	something(stack);
569 	assert(stack == [1, 2]);
570 
571 	stack = null;
572 	copy(stack);
573 	assert(stack == [2]);
574 }
575 
576 @system debug unittest
577 {
578 	dupCount = 0;
579 
580 	alias Del = void function(ref int[]) @safe;
581 
582 	int[] stack;
583 
584 	static void f1(ref int[] stack) @safe
585 	{
586 		stack ~= 1;
587 	}
588 
589 	static void f2(ref int[] stack) @safe
590 	{
591 		stack ~= 2;
592 	}
593 
594 	Multicast!Del something;
595 	something ~= &f1;
596 	something ~= &f2;
597 	assert(dupCount == 0);
598 
599 	Multicast!Del copy = something;
600 	assert(dupCount == 0);
601 	something(stack);
602 	copy(stack);
603 	assert(dupCount == 0);
604 
605 	copy.remove(&f1);
606 	assert(dupCount == 1);
607 }
608 
609 @safe unittest
610 {
611 	alias Del = void function();
612 	Multicast!Del something;
613 	Multicast!Del somethingNull = null;
614 	something = null;
615 
616 	MulticastOpt!Del somethingOpt;
617 	MulticastOpt!Del somethingOptNull = null;
618 	somethingOpt = null;
619 
620 	const MulticastOpt!Del somethingConstNull = null;
621 	immutable MulticastOpt!Del somethingImmutableNull = null;
622 }