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 2fed7ffb3..88438725d 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 @@ -11,13 +11,54 @@ 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. + A placeholder for an object created in Haxe. You can call the object's + functions using `callN("functionName")`, where N 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`. + Caution: the Haxe function will run on whichever thread you call it from. + Java code typically runs on the UI thread, not Haxe's main thread, which can + easily cause thread-related errors. This cannot be easily remedied using Java + code, but is fixable in Haxe using `lime.system.JNI.JNISafety`. + + Sample usage: + + ```haxe + // MyHaxeObject.hx + import lime.system.JNI; + + class MyHaxeObject implements JNISafety + { + @:runOnMainThread + public function onActivityResult(requestCode:Int, resultCode:Int):Void + { + // Insert code to process the result. This code will safely run on the + // main Haxe thread. + } + } + ``` + + ```java + // MyJavaTool.java + import android.content.Intent; + import org.haxe.extension.Extension; + import org.haxe.lime.HaxeObject; + + public class MyJavaTool extends Extension + { + private static var haxeObject:HaxeObject; + + public static function registerHaxeObject(object:HaxeObject) + { + haxeObject = object; + } + + // onActivityResult() always runs on the Android UI thread. + @Override public boolean onActivityResult(int requestCode, int resultCode, Intent data) + { + haxeObject.call2(requestCode, resultCode); + return true; + } + } + ``` **/ public class HaxeObject { diff --git a/src/lime/system/ForegroundWorker.hx b/src/lime/system/ForegroundWorker.hx deleted file mode 100644 index 984ea2d56..000000000 --- a/src/lime/system/ForegroundWorker.hx +++ /dev/null @@ -1,210 +0,0 @@ -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 1d240aff3..ad2577f71 100644 --- a/src/lime/system/JNI.hx +++ b/src/lime/system/JNI.hx @@ -1,7 +1,22 @@ package lime.system; #if (!lime_doc_gen || android) +#if macro +import haxe.macro.Context; +import haxe.macro.Expr; +import haxe.macro.Type; +#else import lime._internal.backend.native.NativeCFFI; +#end +#if !lime_doc_gen +#if target.threaded +import sys.thread.Thread; +#elseif cpp +import cpp.vm.Thread; +#elseif neko +import neko.vm.Thread; +#end +#end /** The Java Native Interface (JNI) allows C++ code to call Java functions, and @@ -19,7 +34,7 @@ import lime._internal.backend.native.NativeCFFI; 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`. + Haxe code can avoid the problem using `lime.system.JNI.JNISafety`. **/ #if !lime_debug @:fileXml('tags="haxe,release"') @@ -325,4 +340,126 @@ class JNIMethod } } } + +/** + Most times a Java class calls a Haxe function, it does so on the UI thread, + which can lead to thread-related errors. These errors can be avoided by + switching back to the main thread before executing any code. + + Usage: + + ```haxe + class MyClass implements JNISafety + { + @:runOnMainThread + public function callbackFunction(data:Dynamic):Void + { + // Code here is guaranteed to run on Haxe's main thread. It's safe + // to call `callbackFunction` via JNI. + } + + public function notACallbackFunction():Void + { + // Code here will run on whichever thread calls the function. It may + // not be safe to call `notACallbackFunction` via JNI. + } + } + ``` +**/ +// Haxe 3 can't parse "target.threaded" inside parentheses. +#if !lime_doc_gen +#if target.threaded +@:autoBuild(lime.system.JNI.JNISafetyTools.build()) +#elseif (cpp || neko) +@:autoBuild(lime.system.JNI.JNISafetyTools.build()) +#end +#end +interface JNISafety {} + +#if !lime_doc_gen +class JNISafetyTools +{ + #if target.threaded + private static var mainThread:Thread = Thread.current(); + #elseif (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 + return Thread.current() == mainThread; + #elseif (cpp || neko) + return Thread.current() == mainThread; + #else + return true; + #end + } + + public static macro function build():Array + { + var fields:Array = Context.getBuildFields(); + + #if macro + for (field in fields) + { + // Don't modify constructors. + if (field.name == "new") + { + continue; + } + + // Don't modify functions lacking `@:runOnMainThread`. + if (field.meta == null || !Lambda.exists(field.meta, + function(meta) return meta.name == ":runOnMainThread")) + { + continue; + } + + switch (field.kind) + { + case FFun(f): + // The function needs to call itself and can't be inline. + field.access.remove(AInline); + + // Make sure there's no return value. + switch (f.ret) + { + case macro:Void: + // Good to go. + case null: + f.ret = macro:Void; + default: + Context.error("Expected return type Void, got " + + new haxe.macro.Printer().printComplexType(f.ret) + ".", field.pos); + } + + var args:Array = []; + for (arg in f.args) + { + args.push(macro $i{arg.name}); + + // Account for an unlikely edge case. + if (arg.name == field.name) + Context.error('${field.name}() should not take an argument named ${field.name}.', field.pos); + } + + // Check the thread before running the function. + f.expr = macro + if (!lime.system.JNI.JNISafetyTools.onMainThread()) + haxe.MainLoop.runInMainThread($i{field.name}.bind($a{args})) + else + ${f.expr}; + default: + } + } + #end + + return fields; + } +} +#end #end