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;
|
import java.lang.Double;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
An object that was originally created by Haxe code. You can call its
|
A placeholder for an object created in Haxe. You can call the object's
|
||||||
functions using `callX("functionName")`, where X is the number of arguments.
|
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
|
Caution: the Haxe function will run on whichever thread you call it from.
|
||||||
cases, this will be the UI thread, which is _not_ the same as Haxe's main
|
Java code typically runs on the UI thread, not Haxe's main thread, which can
|
||||||
thread. To avoid unpredictable thread-related errors, consider using a
|
easily cause thread-related errors. This cannot be easily remedied using Java
|
||||||
`lime.system.ForegroundWorker` as your `HaxeObject`.
|
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
|
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;
|
package lime.system;
|
||||||
|
|
||||||
#if (!lime_doc_gen || android)
|
#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;
|
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
|
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
|
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
|
you can get thread-related errors in both directions. Java functions can
|
||||||
use `Extension.callbackHandler.post()` to switch to the UI thread, while
|
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
|
#if !lime_debug
|
||||||
@:fileXml('tags="haxe,release"')
|
@: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
|
#end
|
||||||
|
|||||||
Reference in New Issue
Block a user