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/_internal/backend/native/NativeApplication.hx b/src/lime/_internal/backend/native/NativeApplication.hx index fdb53733c..af2f38d29 100644 --- a/src/lime/_internal/backend/native/NativeApplication.hx +++ b/src/lime/_internal/backend/native/NativeApplication.hx @@ -600,6 +600,18 @@ class NativeApplication }); } } + + #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.2 && target.threaded) + @:privateAccess haxe.EntryPoint.processEvents(); + #end + #else + @:privateAccess haxe.EntryPoint.processEvents(); + #end #end } } diff --git a/src/lime/system/ForegroundWorker.hx b/src/lime/system/ForegroundWorker.hx new file mode 100644 index 000000000..984ea2d56 --- /dev/null +++ b/src/lime/system/ForegroundWorker.hx @@ -0,0 +1,210 @@ +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. + 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: + + ```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): + field.access.remove(AInline); + + 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 +} 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