Merge pull request #1534 from player-03/ForegroundWorker

Add thread management tools for Android
This commit is contained in:
player-03
2022-06-03 18:25:09 -04:00
committed by GitHub
4 changed files with 249 additions and 0 deletions

View File

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

View File

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

View File

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

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