From 7c7f284a8d1df00c10715cf20d562dead6d18af0 Mon Sep 17 00:00:00 2001 From: Zeta <53486764+Apprentice-Alchemist@users.noreply.github.com> Date: Thu, 24 Nov 2022 10:33:00 +0100 Subject: [PATCH] Atomic operations. (#10610) * Atomic operations. * Fix hashlink's atomic operations. * Add a target.atomics define. * Prevent complaint about "no assertions". * Fix doc gen. * Try to fix things... * Rework api a bit. * Add threaded atomic test, fix C#, fix a method signature. * Make AtomicInt work on cppia, add JS implementation. * Remove atomics on C#. * Fix doc gen, again. * `do ... while` loop for Java. Add some `inline`, and remove some unneeded `extern`. * Fix a potential race condition in the Java impl. * Fix NativeStackTrace.callStack for hl 1.12+. * Use cpp.AtomicInt instead of Int. * Update AtomicInt.hx * [hashlink] use library functions instead opcodes * Fix typo and add warning note to AtomicObject. * [hl] Fix AtomicObject, and bump hl-ver to make tests run. * Add AtomicBool * Inline AtomicBool * Add C# atomics back. * Fix doc gen for atomic bool. * Fix typo. * Add AtomicBool unit test and fix typo. * Fix hxcpp. * Remove leftover debug printf. * Try to fix the C# CI failure. --- extra/ImportAll.hx | 3 + src/context/common.ml | 21 +++- src/generators/hlcode.ml | 1 - src/generators/hlinterp.ml | 2 +- std/cpp/_std/haxe/atomic/AtomicInt.hx | 46 +++++++++ std/cpp/cppia/HostClasses.hx | 1 + std/cs/_std/haxe/atomic/AtomicInt.hx | 61 ++++++++++++ std/cs/_std/haxe/atomic/AtomicObject.hx | 33 +++++++ std/haxe/atomic/AtomicBool.hx | 55 +++++++++++ std/haxe/atomic/AtomicInt.hx | 67 +++++++++++++ std/haxe/atomic/AtomicObject.hx | 44 +++++++++ std/hl/Atomics.hx | 19 ++++ std/hl/_std/haxe/NativeStackTrace.hx | 2 +- std/hl/_std/haxe/atomic/AtomicInt.hx | 49 +++++++++ std/hl/_std/haxe/atomic/AtomicObject.hx | 31 ++++++ std/java/_std/haxe/atomic/AtomicBool.hx | 34 +++++++ std/java/_std/haxe/atomic/AtomicInt.hx | 64 ++++++++++++ std/java/_std/haxe/atomic/AtomicObject.hx | 34 +++++++ std/js/_std/haxe/atomic/AtomicInt.hx | 46 +++++++++ std/js/lib/Atomics.hx | 99 +++++++++++++++++++ std/js/lib/SharedArrayBuffer.hx | 16 +++ tests/threads/build.hxml | 3 +- tests/threads/src/cases/TestAtomics.hx | 23 +++++ tests/unit/compile-hl.hxml | 2 +- .../unitstd/haxe/atomic/AtomicBool.unit.hx | 18 ++++ .../src/unitstd/haxe/atomic/AtomicInt.unit.hx | 33 +++++++ .../unitstd/haxe/atomic/AtomicObject.unit.hx | 15 +++ 27 files changed, 812 insertions(+), 10 deletions(-) create mode 100644 std/cpp/_std/haxe/atomic/AtomicInt.hx create mode 100644 std/cs/_std/haxe/atomic/AtomicInt.hx create mode 100644 std/cs/_std/haxe/atomic/AtomicObject.hx create mode 100644 std/haxe/atomic/AtomicBool.hx create mode 100644 std/haxe/atomic/AtomicInt.hx create mode 100644 std/haxe/atomic/AtomicObject.hx create mode 100644 std/hl/Atomics.hx create mode 100644 std/hl/_std/haxe/atomic/AtomicInt.hx create mode 100644 std/hl/_std/haxe/atomic/AtomicObject.hx create mode 100644 std/java/_std/haxe/atomic/AtomicBool.hx create mode 100644 std/java/_std/haxe/atomic/AtomicInt.hx create mode 100644 std/java/_std/haxe/atomic/AtomicObject.hx create mode 100644 std/js/_std/haxe/atomic/AtomicInt.hx create mode 100644 std/js/lib/Atomics.hx create mode 100644 std/js/lib/SharedArrayBuffer.hx create mode 100644 tests/threads/src/cases/TestAtomics.hx create mode 100644 tests/unit/src/unitstd/haxe/atomic/AtomicBool.unit.hx create mode 100644 tests/unit/src/unitstd/haxe/atomic/AtomicInt.unit.hx create mode 100644 tests/unit/src/unitstd/haxe/atomic/AtomicObject.unit.hx diff --git a/extra/ImportAll.hx b/extra/ImportAll.hx index c445e3edfdc..b6b63a02104 100644 --- a/extra/ImportAll.hx +++ b/extra/ImportAll.hx @@ -99,6 +99,9 @@ class ImportAll { case "haxe.remoting.SyncSocketConnection": if( !(Context.defined("neko") || Context.defined("php") || Context.defined("cpp")) ) continue; case "neko.vm.Ui" | "sys.db.Sqlite" | "sys.db.Mysql" if ( Context.defined("interp") ): continue; case "sys.db.Sqlite" | "sys.db.Mysql" | "cs.db.AdoNet" if ( Context.defined("cs") ): continue; + case "haxe.atomic.AtomicBool" if(!Context.defined("target.atomics")): continue; + case "haxe.atomic.AtomicInt" if(!Context.defined("target.atomics")): continue; + case "haxe.atomic.AtomicObject" if(!Context.defined("target.atomics") || Context.defined("js") || Context.defined("cpp")): continue; } Context.getModule(cl); } else if( sys.FileSystem.isDirectory(p + "/" + file) ) diff --git a/src/context/common.ml b/src/context/common.ml index 91bcac9abbf..836c654771e 100644 --- a/src/context/common.ml +++ b/src/context/common.ml @@ -163,6 +163,8 @@ type platform_config = { pf_exceptions : exceptions_config; (** the scoping of local variables *) pf_scoping : var_scoping_config; + (** target supports atomic operations via haxe.Atomic **) + pf_supports_atomics : bool; } class compiler_callbacks = object(self) @@ -539,7 +541,8 @@ let default_config = pf_scoping = { vs_scope = BlockScope; vs_flags = []; - } + }; + pf_supports_atomics = false; } let get_config com = @@ -569,7 +572,8 @@ let get_config com = vs_flags = (if defined Define.JsUnflatten then ReserveAllTopLevelSymbols else ReserveAllTypesFlat) :: if es6 then [NoShadowing; SwitchCasesNoBlocks;] else [VarHoisting; NoCatchVarShadowing]; - } + }; + pf_supports_atomics = true; } | Lua -> { @@ -652,7 +656,8 @@ let get_config com = pf_scoping = { default_config.pf_scoping with vs_flags = [NoShadowing]; vs_scope = FunctionScope; - } + }; + pf_supports_atomics = true; } | Cs -> { @@ -680,6 +685,7 @@ let get_config com = vs_scope = FunctionScope; vs_flags = [NoShadowing] }; + pf_supports_atomics = true; } | Java -> { @@ -709,7 +715,8 @@ let get_config com = { vs_scope = FunctionScope; vs_flags = [NoShadowing; ReserveAllTopLevelSymbols; ReserveNames(["_"])]; - } + }; + pf_supports_atomics = true; } | Python -> { @@ -740,6 +747,7 @@ let get_config com = pf_capture_policy = CPWrapRef; pf_pad_nulls = true; pf_supports_threads = true; + pf_supports_atomics = true; } | Eval -> { @@ -937,7 +945,10 @@ let init_platform com pf = raw_define com "target.unicode"; end; raw_define_value com.defines "target.name" name; - raw_define com name + raw_define com name; + if com.config.pf_supports_atomics then begin + raw_define com "target.atomics" + end let set_platform com pf file = if com.platform <> Cross then failwith "Multiple targets"; diff --git a/src/generators/hlcode.ml b/src/generators/hlcode.ml index 5f311a08606..733a6b10e05 100644 --- a/src/generators/hlcode.ml +++ b/src/generators/hlcode.ml @@ -572,7 +572,6 @@ let ostr fstr o = | ORefData (r,d) -> Printf.sprintf "refdata %d, %d" r d | ORefOffset (r,r2,off) -> Printf.sprintf "refoffset %d, %d, %d" r r2 off | ONop s -> if s = "" then "nop" else "nop " ^ s - let fundecl_name f = if snd f.fpath = "" then "fun$" ^ (string_of_int f.findex) else (fst f.fpath) ^ "." ^ (snd f.fpath) let dump pr code = diff --git a/src/generators/hlinterp.ml b/src/generators/hlinterp.ml index d795dc542b2..f6fc7f32fd0 100644 --- a/src/generators/hlinterp.ml +++ b/src/generators/hlinterp.ml @@ -2546,7 +2546,7 @@ let check code macros = reg r (rtype r2); reg off HI32; | ONop _ -> - () + (); ) f.code (* TODO : check that all path correctly initialize NULL values and reach a return *) in diff --git a/std/cpp/_std/haxe/atomic/AtomicInt.hx b/std/cpp/_std/haxe/atomic/AtomicInt.hx new file mode 100644 index 00000000000..4e56dd44350 --- /dev/null +++ b/std/cpp/_std/haxe/atomic/AtomicInt.hx @@ -0,0 +1,46 @@ +package haxe.atomic; + +#if cppia +extern +#end +abstract AtomicInt(cpp.Pointer) { + public #if !(scriptable || cppia) inline #end function new(value:Int) { + this = cpp.Pointer.ofArray([value]); + } + + public #if !(scriptable || cppia) inline #end function add(b:Int):Int { + return untyped __cpp__("_hx_atomic_add({0}, {1})", this, b); + } + + public #if !(scriptable || cppia) inline #end function sub(b:Int):Int { + return untyped __cpp__("_hx_atomic_sub({0}, {1})", this, b); + } + + public #if !(scriptable || cppia) inline #end function and(b:Int):Int { + return untyped __cpp__("_hx_atomic_and({0}, {1})", this, b); + } + + public #if !(scriptable || cppia) inline #end function or(b:Int):Int { + return untyped __cpp__("_hx_atomic_or({0}, {1})", this, b); + } + + public #if !(scriptable || cppia) inline #end function xor(b:Int):Int { + return untyped __cpp__("_hx_atomic_xor({0}, {1})", this, b); + } + + public #if !(scriptable || cppia) inline #end function compareExchange(expected:Int, replacement:Int):Int { + return untyped __cpp__("_hx_atomic_compare_exchange({0}, {1}, {2})", this, expected, replacement); + } + + public #if !(scriptable || cppia) inline #end function exchange(value:Int):Int { + return untyped __cpp__("_hx_atomic_exchange({0}, {1})", this, value); + } + + public #if !(scriptable || cppia) inline #end function load():Int { + return untyped __cpp__("_hx_atomic_load({0})", this); + } + + public #if !(scriptable || cppia) inline #end function store(value:Int):Int { + return untyped __cpp__("_hx_atomic_store({0}, {1})", this, value); + } +} diff --git a/std/cpp/cppia/HostClasses.hx b/std/cpp/cppia/HostClasses.hx index 507ee7dbf8f..b5674f85be6 100644 --- a/std/cpp/cppia/HostClasses.hx +++ b/std/cpp/cppia/HostClasses.hx @@ -150,6 +150,7 @@ class HostClasses { "List", "Map", "String", + "haxe.atomic.AtomicInt" ]; static function parseClassInfo(externs:Map, filename:String) { diff --git a/std/cs/_std/haxe/atomic/AtomicInt.hx b/std/cs/_std/haxe/atomic/AtomicInt.hx new file mode 100644 index 00000000000..cf468a872c4 --- /dev/null +++ b/std/cs/_std/haxe/atomic/AtomicInt.hx @@ -0,0 +1,61 @@ +package haxe.atomic; + +private class IntWrapper { + public var value:Int; + + public function new(value:Int) { + this.value = value; + } +} + +abstract AtomicInt(IntWrapper) to IntWrapper { + public inline function new(value:Int) { + this = new IntWrapper(value); + } + + private inline function cas_loop(value:Int, op:(a:Int, b:Int) -> Int):Int { + var oldValue; + var newValue; + do { + oldValue = load(); + newValue = op(oldValue, value); + } while(compareExchange(oldValue, newValue) != oldValue); + return oldValue; + } + + public inline function add(b:Int):Int { + return cas_loop(b, (a, b) -> a + b); + } + + public inline function sub(b:Int):Int { + return cas_loop(b, (a, b) -> a - b); + } + + public inline function and(b:Int):Int { + return cas_loop(b, (a, b) -> cast a & b); + } + + public inline function or(b:Int):Int { + return cas_loop(b, (a, b) -> cast a | b); + } + + public inline function xor(b:Int):Int { + return cas_loop(b, (a, b) -> cast a ^ b); + } + + public inline function compareExchange(expected:Int, replacement:Int):Int { + return cs.Syntax.code("System.Threading.Interlocked.CompareExchange(ref ({0}).value, {1}, {2})", this, replacement, expected); + } + + public inline function exchange(value:Int):Int { + return cs.Syntax.code("System.Threading.Interlocked.Exchange(ref ({0}).value, {1})", this, value); + } + + public inline function load():Int { + return this.value; // according to the CLI spec reads and writes are atomic + } + + public inline function store(value:Int):Int { + return this.value = value; // according to the CLI spec reads and writes are atomic + } +} \ No newline at end of file diff --git a/std/cs/_std/haxe/atomic/AtomicObject.hx b/std/cs/_std/haxe/atomic/AtomicObject.hx new file mode 100644 index 00000000000..c7a07bc4cd3 --- /dev/null +++ b/std/cs/_std/haxe/atomic/AtomicObject.hx @@ -0,0 +1,33 @@ +package haxe.atomic; + +import cs.system.threading.Interlocked.*; + +private class ObjectWrapper { + public var value:T; + + public function new(value:T) { + this.value = value; + } +} + +extern abstract AtomicObject(ObjectWrapper) { + public inline function new(value:T) { + this = new ObjectWrapper(value); + } + + public inline function compareExchange(expected:T, replacement:T):T { + return cs.Syntax.code("System.Threading.Interlocked.CompareExchange(ref ({0}).value, {1}, {2})", this, replacement, expected); + } + + public inline function exchange(value:T):T { + return cs.Syntax.code("System.Threading.Interlocked.Exchange(ref ({0}).value, {1})", this, value); + } + + public inline function load():T { + return this.value; // according to the CLI spec reads and writes are atomic + } + + public inline function store(value:T):T { + return this.value = value; // according to the CLI spec reads and writes are atomic + } +} \ No newline at end of file diff --git a/std/haxe/atomic/AtomicBool.hx b/std/haxe/atomic/AtomicBool.hx new file mode 100644 index 00000000000..24549859415 --- /dev/null +++ b/std/haxe/atomic/AtomicBool.hx @@ -0,0 +1,55 @@ +package haxe.atomic; + +#if !(target.atomics || core_api) +#error "Atomic operations are not supported on this target!" +#end + +/** + Atomic boolean. + (js) The Atomics and SharedArrayBuffer objects need to be available. Errors will be thrown if this is not the case. +**/ +@:coreApi +abstract AtomicBool(AtomicInt) { + private inline function toInt(v:Bool):Int { + return v ? 1 : 0; + } + + private inline function toBool(v:Int):Bool { + return v == 1; + } + + public inline function new(value:Bool):Void { + this = new AtomicInt(toInt(value)); + } + + /** + Atomically compares the value of `a` with `expected` and replaces `a` with `replacement` if they are equal.. + Returns the original value of `a`. + **/ + public inline function compareExchange(expected:Bool, replacement:Bool):Bool { + return toBool(this.compareExchange(toInt(expected), toInt(replacement))); + } + + /** + Atomically exchanges `a` with `value`. + Returns the original value of `a`. + **/ + public inline function exchange(value:Bool):Bool { + return toBool(this.exchange(toInt(value))); + } + + /** + Atomically fetches the value of `a`. + **/ + public inline function load():Bool { + return toBool(this.load()); + } + + /** + Atomically stores `value` into `a`. + Returns the value that has been stored. + **/ + public inline function store(value:Bool):Bool { + return toBool(this.store(toInt(value))); + } +} diff --git a/std/haxe/atomic/AtomicInt.hx b/std/haxe/atomic/AtomicInt.hx new file mode 100644 index 00000000000..2c499322bf4 --- /dev/null +++ b/std/haxe/atomic/AtomicInt.hx @@ -0,0 +1,67 @@ +package haxe.atomic; + +#if !(target.atomics || core_api) +#error "This target does not support atomic operations." +#end + +/** + Atomic integer. + (js) The Atomics and SharedArrayBuffer objects need to be available. Errors will be thrown if this is not the case. +**/ +@:coreType +abstract AtomicInt { + public function new(value:Int):Void; + + /** + Atomically adds `b` to `a`. + Returns the original value of `a`. + **/ + public function add(b:Int):Int; + + /** + Atomically substracts `b` from `a`. + Returns the original value of `a`. + **/ + public function sub(b:Int):Int; + + /** + Atomically computes the bitwise and of `a` and `b` and stores it in `a`. + Returns the original value of `a`. + **/ + public function and(b:Int):Int; + + /** + Atomically computes the bitwise or of `a` and `b` and stores it in `a`. + Returns the original value of `a`. + **/ + public function or(b:Int):Int; + + /** + Atomically computes the bitwise xor of `a` and `b` and stores it in `a`. + Returns the original value of `a`. + **/ + public function xor(b:Int):Int; + + /** + Atomically compares the value of `a` with `expected` and replaces `a` with `replacement` if they are equal.. + Returns the original value of `a`. + **/ + public function compareExchange(expected:Int, replacement:Int):Int; + + /** + Atomically exchanges `a` with `value`. + Returns the original value of `a`. + **/ + public function exchange(value:Int):Int; + + /** + Atomically fetches the value of `a`. + **/ + public function load():Int; + + /** + Atomically stores `value` into `a`. + Returns the value that has been stored. + **/ + public function store(value:Int):Int; +} diff --git a/std/haxe/atomic/AtomicObject.hx b/std/haxe/atomic/AtomicObject.hx new file mode 100644 index 00000000000..76cf26069f9 --- /dev/null +++ b/std/haxe/atomic/AtomicObject.hx @@ -0,0 +1,44 @@ +package haxe.atomic; + +#if !(target.atomics || core_api) +#error "This target does not support atomic operations." +#end + +#if (js || hxcpp) +#error "JavaScript and Hxcpp do not support AtomicObject" +#end + +/** + Atomic object. Use with care, this does not magically make it thread-safe to mutate objects. + Not supported on JavaScript or C++. +**/ +@:coreType +abstract AtomicObject { + public function new(value:T):Void; + + /** + Atomically compares the value of `a` with `expected` and replaces `a` with `replacement` if they are equal.. + Returns the original value of `a`. + + Note that comparison is done by reference, and not by value. + While this is expected for most objects, this might give unexpected behaviour for strings. + **/ + public function compareExchange(expected:T, replacement:T):T; + + /** + Atomically exchanges `a` with `value`. + Returns the original value of `a`. + **/ + public function exchange(value:T):T; + + /** + Atomically fetches the value of `a`. + **/ + public function load():T; + + /** + Atomically stores `value` into `a`. + Returns the value that has been stored. + **/ + public function store(value:T):T; +} diff --git a/std/hl/Atomics.hx b/std/hl/Atomics.hx new file mode 100644 index 00000000000..f811da0d37b --- /dev/null +++ b/std/hl/Atomics.hx @@ -0,0 +1,19 @@ +package hl; + +@:hlNative("std", "atomic_") +extern class Atomics { + static function add32(r:hl.Ref, a:Int):Int; + static function sub32(r:hl.Ref, a:Int):Int; + static function and32(r:hl.Ref, a:Int):Int; + static function or32(r:hl.Ref, a:Int):Int; + static function xor32(r:hl.Ref, a:Int):Int; + static function compareExchange32(r:hl.Ref, a:Int, b:Int):Int; + static function exchange32(r:hl.Ref, val:Int):Int; + static function load32(r:hl.Ref):Int; + static function store32(r:hl.Ref, val:Int):Int; + + static function compareExchangePtr(r:hl.Ref, a:Dynamic, b:Dynamic):Dynamic; + static function exchangePtr(r:hl.Ref, val:Dynamic):Dynamic; + static function loadPtr(r:hl.Ref):Dynamic; + static function storePtr(r:hl.Ref, val:Dynamic):Dynamic; +} diff --git a/std/hl/_std/haxe/NativeStackTrace.hx b/std/hl/_std/haxe/NativeStackTrace.hx index e7cf077a142..eada6faf0be 100644 --- a/std/hl/_std/haxe/NativeStackTrace.hx +++ b/std/hl/_std/haxe/NativeStackTrace.hx @@ -29,7 +29,7 @@ class NativeStackTrace { var count = callStackRaw(null); var arr = new NativeArray(count); callStackRaw(arr); - return arr; + return arr.sub(1, arr.length - 1); } @:hlNative("std", "exception_stack_raw") diff --git a/std/hl/_std/haxe/atomic/AtomicInt.hx b/std/hl/_std/haxe/atomic/AtomicInt.hx new file mode 100644 index 00000000000..f1efde11774 --- /dev/null +++ b/std/hl/_std/haxe/atomic/AtomicInt.hx @@ -0,0 +1,49 @@ +package haxe.atomic; + +#if (hl_ver < version("1.13.0") && !doc_gen) +#error "Atomic operations require HL 1.13+" +#end +import hl.Atomics; + +abstract AtomicInt(hl.NativeArray) { + public inline function new(value:Int):Void { + this = new hl.NativeArray(1); + this[0] = value; + } + + public inline function add(b:Int):Int { + return Atomics.add32(this.getRef(), b); + } + + public inline function sub(b:Int):Int { + return Atomics.sub32(this.getRef(), b); + } + + public inline function and(b:Int):Int { + return Atomics.and32(this.getRef(), b); + } + + public inline function or(b:Int):Int { + return Atomics.or32(this.getRef(), b); + } + + public inline function xor(b:Int):Int { + return Atomics.xor32(this.getRef(), b); + } + + public inline function compareExchange(expected:Int, replacement:Int):Int { + return Atomics.compareExchange32(this.getRef(), expected, replacement); + } + + public inline function exchange(value:Int):Int { + return Atomics.exchange32(this.getRef(), value); + } + + public inline function load():Int { + return Atomics.load32(this.getRef()); + } + + public inline function store(value:Int):Int { + return Atomics.store32(this.getRef(), value); + } +} diff --git a/std/hl/_std/haxe/atomic/AtomicObject.hx b/std/hl/_std/haxe/atomic/AtomicObject.hx new file mode 100644 index 00000000000..e0a03afad18 --- /dev/null +++ b/std/hl/_std/haxe/atomic/AtomicObject.hx @@ -0,0 +1,31 @@ +package haxe.atomic; + +#if (hl_ver < version("1.13.0") && !doc_gen) +#error "Atomic operations require HL 1.13+" +#end +import hl.Atomics; + +// use hl.NativeArray instead of hl.NativeArray +// so that the compiler doesn't get confused and emit hl.Ref.make(this.getRef()) +abstract AtomicObject(hl.NativeArray) { + public inline function new(value:T):Void { + this = new hl.NativeArray(1); + this[0] = value; + } + + public inline function compareExchange(expected:T, replacement:T):T { + return Atomics.compareExchangePtr(this.getRef(), expected, replacement); + } + + public inline function exchange(value:T):T { + return Atomics.exchangePtr(this.getRef(), value); + } + + public inline function load():T { + return Atomics.loadPtr(this.getRef()); + } + + public inline function store(value:T):T { + return Atomics.storePtr(this.getRef(), value); + } +} diff --git a/std/java/_std/haxe/atomic/AtomicBool.hx b/std/java/_std/haxe/atomic/AtomicBool.hx new file mode 100644 index 00000000000..2b98caabc4a --- /dev/null +++ b/std/java/_std/haxe/atomic/AtomicBool.hx @@ -0,0 +1,34 @@ +package haxe.atomic; + +import java.util.concurrent.atomic.AtomicBoolean; + +abstract AtomicBool(AtomicBoolean) { + public inline function new(value:Bool) { + this = new AtomicBoolean(value); + } + + public inline function compareExchange(expected:Bool, replacement:Bool):Bool { + // Java's compareAndSet returns a boolean, so do a CAS loop to be able to return the original value without a potential race condition + + var original; + var real_replacement; + do { + original = this.get(); + real_replacement = original == expected ? replacement : original; + } while (!this.compareAndSet(original, real_replacement)); + return original; + } + + public inline function exchange(value:Bool):Bool { + return this.getAndSet(value); + } + + public inline function load():Bool { + return this.get(); + } + + public inline function store(value:Bool):Bool { + this.set(value); + return value; + } +} diff --git a/std/java/_std/haxe/atomic/AtomicInt.hx b/std/java/_std/haxe/atomic/AtomicInt.hx new file mode 100644 index 00000000000..64dd6c177d7 --- /dev/null +++ b/std/java/_std/haxe/atomic/AtomicInt.hx @@ -0,0 +1,64 @@ +package haxe.atomic; + +import java.util.concurrent.atomic.AtomicInteger; + +abstract AtomicInt(AtomicInteger) { + public inline function new(value:Int) { + this = new AtomicInteger(value); + } + + private inline function cas_loop(value:Int, op:(a:Int, b:Int) -> Int):Int { + var val; + + do { + val = this.get(); + } while (!this.compareAndSet(val, op(val, value))); + + return val; + } + + public inline function add(b:Int):Int { + return this.getAndAdd(b); + } + + public inline function sub(b:Int):Int { + return this.getAndAdd(-b); + } + + public inline function and(b:Int):Int { + return cas_loop(b, (a:Int, b:Int) -> a & b); + } + + public inline function or(b:Int):Int { + return cas_loop(b, (a:Int, b:Int) -> a | b); + } + + public inline function xor(b:Int):Int { + return cas_loop(b, (a:Int, b:Int) -> a ^ b); + } + + public inline function compareExchange(expected:Int, replacement:Int):Int { + // Java's compareAndSet returns a boolean, so do a CAS loop to be able to return the original value without a potential race condition + + var original; + var real_replacement; + do { + original = this.get(); + real_replacement = original == expected ? replacement : original; + } while (!this.compareAndSet(original, real_replacement)); + return original; + } + + public inline function exchange(value:Int):Int { + return this.getAndSet(value); + } + + public inline function load():Int { + return this.get(); + } + + public inline function store(value:Int):Int { + this.set(value); + return value; + } +} diff --git a/std/java/_std/haxe/atomic/AtomicObject.hx b/std/java/_std/haxe/atomic/AtomicObject.hx new file mode 100644 index 00000000000..2bccf7a1ec4 --- /dev/null +++ b/std/java/_std/haxe/atomic/AtomicObject.hx @@ -0,0 +1,34 @@ +package haxe.atomic; + +import java.util.concurrent.atomic.AtomicReference; + +abstract AtomicObject(AtomicReference) { + public inline function new(value:T) { + this = new AtomicReference(value); + } + + public inline function compareExchange(expected:T, replacement:T):T { + // Java's compareAndSet returns a boolean, so do a CAS loop to be able to return the original value without a potential race condition + + var original; + var real_replacement; + do { + original = this.get(); + real_replacement = original == expected ? replacement : original; + } while (!this.compareAndSet(original, real_replacement)); + return original; + } + + public inline function exchange(value:T):T { + return this.getAndSet(value); + } + + public inline function load():T { + return this.get(); + } + + public inline function store(value:T):T { + this.set(value); + return value; + } +} diff --git a/std/js/_std/haxe/atomic/AtomicInt.hx b/std/js/_std/haxe/atomic/AtomicInt.hx new file mode 100644 index 00000000000..a6510bddd80 --- /dev/null +++ b/std/js/_std/haxe/atomic/AtomicInt.hx @@ -0,0 +1,46 @@ +package haxe.atomic; + +import js.lib.Atomics; + +abstract AtomicInt(js.lib.Int32Array) to js.lib.Int32Array { + public inline function new(value:Int) { + this = new js.lib.Int32Array(new js.lib.SharedArrayBuffer(js.lib.Int32Array.BYTES_PER_ELEMENT)); + this[0] = value; + } + + public inline function add(b:Int):Int { + return Atomics.add(this, 0, b); + } + + public inline function sub(b:Int):Int { + return Atomics.sub(this, 0, b); + } + + public inline function and(b:Int):Int { + return Atomics.and(this, 0, b); + } + + public inline function or(b:Int):Int { + return Atomics.or(this, 0, b); + } + + public inline function xor(b:Int):Int { + return Atomics.xor(this, 0, b); + } + + public inline function compareExchange(expected:Int, replacement:Int):Int { + return Atomics.compareExchange(this, 0, expected, replacement); + } + + public inline function exchange(value:Int):Int { + return Atomics.exchange(this, 0, value); + } + + public inline function load():Int { + return Atomics.load(this, 0); + } + + public inline function store(value:Int):Int { + return Atomics.store(this, 0, value); + } +} diff --git a/std/js/lib/Atomics.hx b/std/js/lib/Atomics.hx new file mode 100644 index 00000000000..0fb148d14d5 --- /dev/null +++ b/std/js/lib/Atomics.hx @@ -0,0 +1,99 @@ +package js.lib; + +private typedef E = haxe.extern.EitherType; +private typedef IntTypedArray = E>>>>; + +/** + The Atomics object provides atomic operations as static methods. They are used with SharedArrayBuffer and ArrayBuffer objects. + + Documentation [Atomics](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics) by [Mozilla Contributors](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/contributors.txt), licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/). +**/ +@:native("Atomics") +extern class Atomics { + /** + Adds the provided value to the existing value at the specified index of the array. + Returns the old value at that index. + This atomic operation guarantees that no other write happens until the modified value is written back. + **/ + static function add(typedArray:IntTypedArray, index:Int, value:Int):Int; + + /** + Computes a bitwise AND on the value at the specified index of the array with the provided value. + Returns the old value at that index. + This atomic operation guarantees that no other write happens until the modified value is written back. + **/ + static function and(typedArray:IntTypedArray, index:Int, value:Int):Int; + + /** + Stores a value at the specified index of the array, if it equals a value. + Returns the old value. + This atomic operation guarantees that no other write happens until the modified value is written back. + **/ + static function compareExchange(typedArray:IntTypedArray, index:Int, expectedValue:Int, replacementValue:Int):Int; + + /** + Stores a value at the specified index of the array. + Returns the old value. + This atomic operation guarantees that no other write happens until the modified value is written back. + **/ + static function exchange(typedArray:IntTypedArray, index:Int, value:Int):Int; + + /** + An optimization primitive that can be used to determine whether to use locks or atomic operations. + Returns `true` if an atomic operation on arrays of the given element size will be implemented using a hardware atomic operation (as opposed to a lock). Experts only. + **/ + static function isLockFree(size:Int):Bool; + + /** + Returns the value at the specified index of the array. + This atomic operation guarantees that no other write happens until the modified value is written back. + **/ + static function load(typedArray:IntTypedArray, index:Int):Int; + + /** + Notifies agents that are waiting on the specified index of the array. + Returns the number of agents that were notified. + **/ + static function notify(typedArray:IntTypedArray, index:Int, ?count:Int):Int; + + /** + Computes a bitwise OR on the value at the specified index of the array with the provided value. + Returns the old value at that index. + This atomic operation guarantees that no other write happens until the modified value is written back. + **/ + static function or(typedArray:IntTypedArray, index:Int, value:Int):Int; + + /** + Stores a value at the specified index of the array. + Returns the value. + This atomic operation guarantees that no other write happens until the modified value is written back. + **/ + static function store(typedArray:IntTypedArray, index:Int, value:Int):Int; + + /** + Subtracts a value at the specified index of the array. + Returns the old value at that index. + This atomic operation guarantees that no other write happens until the modified value is written back. + **/ + static function sub(typedArray:IntTypedArray, index:Int, value:Int):Int; + + /** + Verifies that the specified index of the array still contains a value and sleeps awaiting or times out. + Returns either "ok", "not-equal", or "timed-out". If waiting is not allowed in the calling agent then it throws an Error exception. + Most browsers will not allow wait() on the browser's main thread.) + **/ + static function wait(typedArray:Int32Array, index:Int, value:Int, ?timeout:Int):WaitValue; + + /** + Computes a bitwise XOR on the value at the specified index of the array with the provided value. + Returns the old value at that index. + This atomic operation guarantees that no other write happens until the modified value is written back. + **/ + static function xor(typedArray:IntTypedArray, index:Int, value:Int):Int; +} + +enum abstract WaitValue(String) { + var OK = "ok"; + var NotEqual = "not-equal"; + var TimedOut = "timed-out"; +} diff --git a/std/js/lib/SharedArrayBuffer.hx b/std/js/lib/SharedArrayBuffer.hx new file mode 100644 index 00000000000..efd8859dc80 --- /dev/null +++ b/std/js/lib/SharedArrayBuffer.hx @@ -0,0 +1,16 @@ +package js.lib; + +/** + The SharedArrayBuffer object is used to represent a generic, fixed-length raw binary data buffer, similar to the ArrayBuffer object, but in a way that they can be used to create views on shared memory. + A SharedArrayBuffer is not a Transferable Object, unlike an ArrayBuffer which is transferable. + + Documentation [SharedArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) by [Mozilla Contributors](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer/contributors.txt), licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/). +**/ +@:native("SharedArrayBuffer") +extern class SharedArrayBuffer { + final byteLength:Int; + + function new(?length:Int):Void; + + function slice(?begin:Int, ?end:Int):ArrayBuffer; +} diff --git a/tests/threads/build.hxml b/tests/threads/build.hxml index 77b36290910..b4d1f30386f 100644 --- a/tests/threads/build.hxml +++ b/tests/threads/build.hxml @@ -3,4 +3,5 @@ --library utest --dce full -D analyzer-optimize --D UTEST_PRINT_TESTS \ No newline at end of file +-D UTEST_PRINT_TESTS +-D hl-ver=1.13.0 \ No newline at end of file diff --git a/tests/threads/src/cases/TestAtomics.hx b/tests/threads/src/cases/TestAtomics.hx new file mode 100644 index 00000000000..bc720120fcb --- /dev/null +++ b/tests/threads/src/cases/TestAtomics.hx @@ -0,0 +1,23 @@ +package cases; + + +#if target.atomics +import haxe.atomic.AtomicInt; +#end + +@:timeout(2000) +class TestAtomics extends utest.Test { + #if target.atomics + function test(async:Async) { + var a = new AtomicInt(5); + final thread = Thread.create(() -> { + while(a.compareExchange(0, 2) != 0) {} + }); + isTrue(a.compareExchange(5, 0) == 5); + + while (a.compareExchange(2, 2) != 2) {} + + async.done(); + } + #end +} diff --git a/tests/unit/compile-hl.hxml b/tests/unit/compile-hl.hxml index a1fff608b5e..1b169b97539 100644 --- a/tests/unit/compile-hl.hxml +++ b/tests/unit/compile-hl.hxml @@ -3,4 +3,4 @@ compile-each.hxml -hl bin/unit.hl #-D interp -D hl-check --D hl-ver=1.11.0 \ No newline at end of file +-D hl-ver=1.13.0 \ No newline at end of file diff --git a/tests/unit/src/unitstd/haxe/atomic/AtomicBool.unit.hx b/tests/unit/src/unitstd/haxe/atomic/AtomicBool.unit.hx new file mode 100644 index 00000000000..0274f51b16f --- /dev/null +++ b/tests/unit/src/unitstd/haxe/atomic/AtomicBool.unit.hx @@ -0,0 +1,18 @@ +#if (target.atomics) +var a = new haxe.atomic.AtomicBool(true); + +a.load() == true; +a.store(false) == false; +a.load() == false; + +a.compareExchange(false, true) == false; +a.load() == true; + +a.compareExchange(false, false) == true; +a.load() == true; + +a.exchange(true) == true; +a.load() == true; +#else +0 == 0; // prevent "no assertions" warning +#end diff --git a/tests/unit/src/unitstd/haxe/atomic/AtomicInt.unit.hx b/tests/unit/src/unitstd/haxe/atomic/AtomicInt.unit.hx new file mode 100644 index 00000000000..54bbffa95dc --- /dev/null +++ b/tests/unit/src/unitstd/haxe/atomic/AtomicInt.unit.hx @@ -0,0 +1,33 @@ +#if target.atomics +var a = new haxe.atomic.AtomicInt(0); + +a.load() == 0; + +a.store(5) == 5; +a.load() == 5; + +a.add(5) == 5; +a.load() == 10; + +a.sub(5) == 10; +a.load() == 5; + +a.and(20) == 5; +a.load() == 4; + +a.or(3) == 4; +a.load() == 7; + +a.xor(2) == 7; +a.load() == 5; + +a.compareExchange(0, 0) == 5; +a.load() == 5; +a.compareExchange(5, 0) == 5; +a.load() == 0; + +a.exchange(10) == 0; +a.load() == 10; +#else +0 == 0; // prevent "no assertions" warning +#end \ No newline at end of file diff --git a/tests/unit/src/unitstd/haxe/atomic/AtomicObject.unit.hx b/tests/unit/src/unitstd/haxe/atomic/AtomicObject.unit.hx new file mode 100644 index 00000000000..7f95e195f7f --- /dev/null +++ b/tests/unit/src/unitstd/haxe/atomic/AtomicObject.unit.hx @@ -0,0 +1,15 @@ +#if (target.atomics && !(js || cpp)) +var a = new haxe.atomic.AtomicObject("Hey World!"); + +a.load() == "Hey World!"; +a.store("Hello World!") == "Hello World!"; +a.load() == "Hello World!"; + +a.compareExchange("Hello World!", "Goodbye World!") == "Hello World!"; +a.load() == "Goodbye World!"; + +a.exchange("Hello World!") == "Goodbye World!"; +a.load() == "Hello World!"; +#else +0 == 0; // prevent "no assertions" warning +#end