From 7890951d12186e86c5a6acedcbfb3b715b8a8fb8 Mon Sep 17 00:00:00 2001 From: Joseph Cloutier Date: Mon, 30 May 2022 16:37:43 -0400 Subject: [PATCH 1/7] Add `MainLoop` support. Pros: It's a standard Haxe feature that other Haxelibs may rely on. Plus it offers built-in thread safety, unlike `onUpdate`. Cons: It incurs two `mutex.acquire()` calls per frame. --- src/lime/_internal/backend/native/NativeApplication.hx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/lime/_internal/backend/native/NativeApplication.hx b/src/lime/_internal/backend/native/NativeApplication.hx index c909e1f63..478c07299 100644 --- a/src/lime/_internal/backend/native/NativeApplication.hx +++ b/src/lime/_internal/backend/native/NativeApplication.hx @@ -600,6 +600,16 @@ class NativeApplication }); } } + + #if target.threaded + sys.thread.Thread.current().events.progress(); + #elseif cpp + cpp.vm.Thread.current().events.progress(); + #elseif neko + neko.vm.Thread.current().events.progress(); + #else + @:privateAccess haxe.EntryPoint.processEvents(); + #end #end } } From 6e86e45851d14295d9221d52af15c22d06f64f83 Mon Sep 17 00:00:00 2001 From: Joseph Cloutier Date: Tue, 31 May 2022 01:06:56 -0400 Subject: [PATCH 2/7] Implement `ForegroundWorker`. --- .../main/java/org/haxe/lime/HaxeObject.java | 9 + src/lime/system/ForegroundWorker.hx | 206 ++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 src/lime/system/ForegroundWorker.hx diff --git a/dependencies/extension-api/src/main/java/org/haxe/lime/HaxeObject.java b/dependencies/extension-api/src/main/java/org/haxe/lime/HaxeObject.java index d3872000c..2fed7ffb3 100644 --- a/dependencies/extension-api/src/main/java/org/haxe/lime/HaxeObject.java +++ b/dependencies/extension-api/src/main/java/org/haxe/lime/HaxeObject.java @@ -10,6 +10,15 @@ import java.lang.Long; import java.lang.Float; import java.lang.Double; +/** + An object that was originally created by Haxe code. You can call its + functions using `callX("functionName")`, where X is the number of arguments. + + Caution: the Haxe function will run on the thread you called it from. In many + cases, this will be the UI thread, which is _not_ the same as Haxe's main + thread. To avoid unpredictable thread-related errors, consider using a + `lime.system.ForegroundWorker` as your `HaxeObject`. +**/ public class HaxeObject { public long __haxeHandle; diff --git a/src/lime/system/ForegroundWorker.hx b/src/lime/system/ForegroundWorker.hx new file mode 100644 index 000000000..686efaa58 --- /dev/null +++ b/src/lime/system/ForegroundWorker.hx @@ -0,0 +1,206 @@ +package lime.system; + +import haxe.macro.Context; +import haxe.macro.Expr; +import haxe.macro.Type; +#if target.threaded +import sys.thread.Thread; +#elseif cpp +import cpp.vm.Thread; +#elseif neko +import neko.vm.Thread; +#end + +/** + An object whose instance functions always run on the main thread. If called + from any other thread, they'll switch to the main thread before proceeding. + + Usage: + + ```haxe + class MyClass extends ForegroundWorker + { + public function foregroundFunction():Void + { + // Code here is guaranteed to run on Haxe's main thread. + } + + @:anyThread public function anyThreadFunction():Void + { + // Code here will run on whichever thread calls the function, thanks + // to `@:anyThread`. + } + } + ``` + + @see `ForegroundWorkerBuilder` for details and more options. +**/ +#if (target.threaded || cpp || neko) +@:autoBuild(lime.system.ForegroundWorkerBuilder.modifyInstanceFunctions()) +#end +// Yes, this could also be an interface, but that opens up edge cases. Better to +// leave those for advanced users who use `ForegroundWorkerBuilder`. +class ForegroundWorker +{ + #if (target.threaded || cpp || neko) + private static var mainThread:Thread = Thread.current(); + #end + + /** + @return Whether the calling function is being run on the main thread. + **/ + public static inline function onMainThread():Bool + { + #if (target.threaded || cpp || neko) + return Thread.current() == mainThread; + #else + return true; + #end + } +} + +class ForegroundWorkerBuilder +{ + /** + A build macro that iterates through a class's instance functions + (excluding those marked `@:anyThread`) and inserts code to ensure these + functions run only on the main thread. + + Caution: build macros never directly modify superclasses. To make a + superclass's functions run on the main thread, either annotate that + class with its own `@:build` metadata or override all of its functions. + + Usage: + + ```haxe + @:build(lime.system.ForegroundWorker.ForegroundWorkerBuilder.modifyInstanceFunctions()) + class MyClass + { + public var array0:Array = []; + private var array1:Array = []; + + public function copyItems():Void + { + //Thread safety code will be inserted automatically. You can + //write thread-unsafe code as normal. + for (i in 0...array0.length) + if (this.array0[i] != null) + this.array1.push(this.array0[i]); + } + } + ``` + **/ + public static macro function modifyInstanceFunctions():Array + { + var fields:Array = Context.getBuildFields(); + + for (field in fields) + { + if (field.access.indexOf(AStatic) >= 0) + continue; + + modifyField(field); + } + + return fields; + } + + /** + A build macro that iterates through a class's static functions + (excluding those marked `@:anyThread`) and inserts code to ensure these + functions run only on the main thread. + + Usage: + + ```haxe + @:build(lime.system.ForegroundWorker.ForegroundWorkerBuilder.modifyStaticFunctions()) + class MyClass + { + private static var eventCount:Map = new Map(); + public static function countEvent(event:String):Void + { + //Thread safety code will be inserted automatically. You can + //write thread-unsafe code as normal. + if (eventCount.exists(event)) + eventCount[event]++; + else + eventCount[event] = 1; + } + } + ``` + **/ + public static macro function modifyStaticFunctions():Array + { + var fields:Array = Context.getBuildFields(); + + for (field in fields) + { + if (field.access.indexOf(AStatic) < 0) + continue; + + modifyField(field); + } + + return fields; + } + + #if macro + private static function modifyField(field:Field):Void + { + if (field.name == "new") + return; + + if (field.meta != null) + { + for (meta in field.meta) + { + if (meta.name == ":anyThread") + return; + } + } + + switch (field.kind) + { + case FFun(f): + var qualifiedIdent:Array; + if (field.access.indexOf(AStatic) >= 0) + { + if (Context.getLocalClass() == null) + throw "ForegroundWorkerBuilder is only designed to work on classes."; + + var localClass:ClassType = Context.getLocalClass().get(); + qualifiedIdent = localClass.pack.copy(); + if (localClass.module != localClass.name) + qualifiedIdent.push(localClass.module); + qualifiedIdent.push(localClass.name); + } + else + { + qualifiedIdent = []; + } + qualifiedIdent.push(field.name); + + var args:Array = [for (arg in f.args) macro $i{arg.name}]; + + var exprs:Array; + switch (f.expr.expr) + { + case EBlock(e): + exprs = e; + default: + exprs = [f.expr]; + f.expr = { pos: field.pos, expr: EBlock(exprs) }; + } + + exprs.unshift(macro + if (!lime.system.ForegroundWorker.onMainThread()) + { + haxe.MainLoop.runInMainThread($p{qualifiedIdent}.bind($a{args})); + return; + } + ); + default: + } + } + #end +} From 312dd70d72cdb785c88ac206063d7139b602ac3a Mon Sep 17 00:00:00 2001 From: Joseph Cloutier Date: Tue, 31 May 2022 02:06:34 -0400 Subject: [PATCH 3/7] Never inline `ForegroundWorker` functions. The early `return` is incompatible with it. --- src/lime/system/ForegroundWorker.hx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lime/system/ForegroundWorker.hx b/src/lime/system/ForegroundWorker.hx index 686efaa58..084f48ad0 100644 --- a/src/lime/system/ForegroundWorker.hx +++ b/src/lime/system/ForegroundWorker.hx @@ -162,6 +162,8 @@ class ForegroundWorkerBuilder switch (field.kind) { case FFun(f): + field.access.remove(AInline); + var qualifiedIdent:Array; if (field.access.indexOf(AStatic) >= 0) { From 894445687e6c0207434a188802e1a5524e67b871 Mon Sep 17 00:00:00 2001 From: Joseph Cloutier Date: Tue, 31 May 2022 02:31:59 -0400 Subject: [PATCH 4/7] Remove nonexistent function calls. Earlier Haxe versions just used `EntryPoint`. --- src/lime/_internal/backend/native/NativeApplication.hx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/lime/_internal/backend/native/NativeApplication.hx b/src/lime/_internal/backend/native/NativeApplication.hx index 478c07299..d3bd95f44 100644 --- a/src/lime/_internal/backend/native/NativeApplication.hx +++ b/src/lime/_internal/backend/native/NativeApplication.hx @@ -601,12 +601,8 @@ class NativeApplication } } - #if target.threaded + #if (haxe_ver > "4.1.5" && target.threaded) sys.thread.Thread.current().events.progress(); - #elseif cpp - cpp.vm.Thread.current().events.progress(); - #elseif neko - neko.vm.Thread.current().events.progress(); #else @:privateAccess haxe.EntryPoint.processEvents(); #end From 8015148ee07ea8c5eaeb4c8bacf28f8d91968f06 Mon Sep 17 00:00:00 2001 From: Joseph Cloutier Date: Tue, 31 May 2022 02:37:52 -0400 Subject: [PATCH 5/7] Fix conditional compilation. For real this time, definitely, hopefully, maybe. --- src/lime/_internal/backend/native/NativeApplication.hx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lime/_internal/backend/native/NativeApplication.hx b/src/lime/_internal/backend/native/NativeApplication.hx index d3bd95f44..0219b7571 100644 --- a/src/lime/_internal/backend/native/NativeApplication.hx +++ b/src/lime/_internal/backend/native/NativeApplication.hx @@ -601,9 +601,15 @@ class NativeApplication } } - #if (haxe_ver > "4.1.5" && target.threaded) + #if (haxe_ver > "4.1.5") + #if target.threaded sys.thread.Thread.current().events.progress(); #else + // Duplicate code required because Haxe 3 can't handle + // #if (haxe_ver > "4.1.5" && target.threaded) + @:privateAccess haxe.EntryPoint.processEvents(); + #end + #else @:privateAccess haxe.EntryPoint.processEvents(); #end #end From c48f1fb44abaec254435c7c6ed26da62e19b58a3 Mon Sep 17 00:00:00 2001 From: Joseph Cloutier Date: Tue, 31 May 2022 02:57:02 -0400 Subject: [PATCH 6/7] Try a different approach to `haxe_ver`. --- src/lime/_internal/backend/native/NativeApplication.hx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lime/_internal/backend/native/NativeApplication.hx b/src/lime/_internal/backend/native/NativeApplication.hx index 0219b7571..c38f5f251 100644 --- a/src/lime/_internal/backend/native/NativeApplication.hx +++ b/src/lime/_internal/backend/native/NativeApplication.hx @@ -601,12 +601,12 @@ class NativeApplication } } - #if (haxe_ver > "4.1.5") + #if (haxe_ver >= 4.2) #if target.threaded sys.thread.Thread.current().events.progress(); #else // Duplicate code required because Haxe 3 can't handle - // #if (haxe_ver > "4.1.5" && target.threaded) + // #if (haxe_ver >= 4.2 && target.threaded) @:privateAccess haxe.EntryPoint.processEvents(); #end #else From f70b43a543e05ff239499752a8cd4e55da7e8f1e Mon Sep 17 00:00:00 2001 From: Joseph Cloutier Date: Fri, 3 Jun 2022 18:04:32 -0400 Subject: [PATCH 7/7] Explain `ForegroundWorker`'s main use case. --- src/lime/system/ForegroundWorker.hx | 2 ++ src/lime/system/JNI.hx | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/lime/system/ForegroundWorker.hx b/src/lime/system/ForegroundWorker.hx index 084f48ad0..984ea2d56 100644 --- a/src/lime/system/ForegroundWorker.hx +++ b/src/lime/system/ForegroundWorker.hx @@ -14,6 +14,8 @@ import neko.vm.Thread; /** An object whose instance functions always run on the main thread. If called from any other thread, they'll switch to the main thread before proceeding. + This is important for Android apps that use JNI, as most times a Java class + calls a Haxe function, it does so on the UI thread. Usage: diff --git a/src/lime/system/JNI.hx b/src/lime/system/JNI.hx index e1944d3cc..1d240aff3 100644 --- a/src/lime/system/JNI.hx +++ b/src/lime/system/JNI.hx @@ -3,6 +3,24 @@ package lime.system; #if (!lime_doc_gen || android) import lime._internal.backend.native.NativeCFFI; +/** + The Java Native Interface (JNI) allows C++ code to call Java functions, and + vice versa. On Android, Haxe code compiles to C++, but only Java code can + access the Android system API, so it's often necessary to use both. + + For a working example, run `lime create extension MyExtension`, then look at + MyExtension.hx and MyExtension.java. + + You can pass Haxe objects to Java, much like any other data. In Java, + they'll have type `org.haxe.lime.HaxeObject`, meaning the method that + receives them might have signature `(Lorg/haxe/lime/HaxeObject;)V`. Once + sent, the Java class can store the object and call its functions. + + Note that most Java code runs on a different thread than Haxe, meaning that + you can get thread-related errors in both directions. Java functions can + use `Extension.callbackHandler.post()` to switch to the UI thread, while + Haxe code can avoid the problem using `lime.system.ForegroundWorker`. +**/ #if !lime_debug @:fileXml('tags="haxe,release"') @:noDebug