Merge pull request #1552 from player-03/JNI_safety
Rename `ForegroundWorker` → `JNISafety`.
This commit is contained in:
@@ -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
|
||||
{
|
||||
|
||||
@@ -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<String> = [];
|
||||
private var array1:Array<String> = [];
|
||||
|
||||
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<Field>
|
||||
{
|
||||
var fields:Array<Field> = 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<String, Int> = 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<Field>
|
||||
{
|
||||
var fields:Array<Field> = 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<String>;
|
||||
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<Expr> = [for (arg in f.args) macro $i{arg.name}];
|
||||
|
||||
var exprs:Array<Expr>;
|
||||
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
|
||||
}
|
||||
@@ -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<Field>
|
||||
{
|
||||
var fields:Array<Field> = 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<Expr> = [];
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user