Merge pull request #1552 from player-03/JNI_safety

Rename `ForegroundWorker` → `JNISafety`.
This commit is contained in:
player-03
2022-06-18 22:20:08 -04:00
committed by GitHub
3 changed files with 185 additions and 217 deletions

View File

@@ -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
{

View File

@@ -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
}

View File

@@ -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