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 }