haxe icon indicating copy to clipboard operation
haxe copied to clipboard

Support for Java Virtual Threads in jvm target (requires Java 21+)

Open barisyild opened this issue 7 months ago • 10 comments

@Simn I'm experimenting with adding support for Java Virtual Threads (introduced in Java 19 and stabilized in Java 21) in the jvm target. I've created externs and implementation support (see below), and it works well when running on Java 21+. (I haven't tested Java 19)

However, I'm facing a few conceptual and compatibility issue that I'd like feedback on before proceeding further or proposing a PR.

What I’ve done

Added support for an optional isVirtual:Bool flag to Thread.create(...) and Thread.createWithEventLoop(...) to use either a native or virtual thread.

Implemented VirtualHaxeThread and NativeHaxeThread classes behind an interface (IJavaThread), so the core logic remains agnostic to the implementation.

Used the Thread.ofVirtual().unstarted(Runnable) builder API (Java 21+) for virtual thread creation.

Example usage

var thread = Thread.create(() -> {
    // runs as virtual thread
}, true);

Problem

The core issue is compatibility with existing Java versions:

The virtual thread APIs require Java 21+.

hxjava currently supports lower JDK versions (e.g. Java 8+) and generating externs with higher versions can break compatibility with those older JDKs.

hxjava's externs are typically auto-generated from the Java SDK, so adding manual externs for Java 21+ features may be appropriate in the current structure.

Thread Std

/*
 * Copyright (C)2005-2019 Haxe Foundation
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

package sys.thread;

import java.Lib;
import java.lang.Runnable;
import java.util.WeakHashMap;
import java.util.Collections;
import java.lang.Thread as JavaThread;
import java.lang.System;
import java.StdTypes.Int64 as Long;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.LinkedBlockingDeque;

private typedef ThreadImpl = HaxeThread;

abstract Thread(ThreadImpl) from ThreadImpl {
	public var events(get,never):EventLoop;

	inline function new(t:HaxeThread) {
		this = t;
	}

	public static inline function create(job:()->Void, isVirtual:Bool = false):Thread {
		return HaxeThread.create(job, false, isVirtual);
	}

	public static inline function current():Thread {
		return HaxeThread.get(JavaThread.currentThread());
	}

	public static inline function runWithEventLoop(job:()->Void):Void {
		HaxeThread.runWithEventLoop(job);
	}

	public static inline function createWithEventLoop(job:()->Void, isVirtual:Bool = false):Thread {
		return HaxeThread.create(job, true, isVirtual);
	}

	public static inline function readMessage(block:Bool):Dynamic {
		return current().getHandle().readMessage(block);
	}

	public inline function sendMessage(msg:Dynamic):Void {
		this.sendMessage(msg);
	}

	inline function getHandle():HaxeThread {
		return this;
	}

	function get_events():EventLoop {
		if(this.events == null)
			throw new NoEventLoopException();
		return this.events;
	}

	@:keep //TODO: keep only if events are actually used
	static function processEvents():Void {
		current().getHandle().events.loop();
	}
}

private class HaxeThread {
	static var nativeThreads:java.util.Map<JavaThread,HaxeThread>;
	static var mainJavaThread:JavaThread;
	static var mainHaxeThread:HaxeThread;

	static function __init__() {
		nativeThreads = Collections.synchronizedMap(new WeakHashMap<JavaThread,HaxeThread>());
		mainJavaThread = JavaThread.currentThread();
		mainHaxeThread = new HaxeThread();
		mainHaxeThread.events = new EventLoop();
	}

	public final messages = new LinkedBlockingDeque<Dynamic>();

	public var events(default,null):Null<EventLoop>;
	public var isVirtual(default, null):Bool;

	public static function create(job:()->Void, withEventLoop:Bool, isVirtual:Bool = false):HaxeThread {
		var hx = new HaxeThread();
		hx.isVirtual = isVirtual;
		if(withEventLoop)
			hx.events = new EventLoop();

		var thread:IJavaThread;
		if(isVirtual) {
			thread = new VirtualHaxeThread(hx, job, withEventLoop);
		}else{
			thread = new NativeHaxeThread(hx, job, withEventLoop);
		}

		thread.setDaemon(true);
		thread.start();

		return hx;
	}

	public static function get(javaThread:JavaThread):HaxeThread {
		if(javaThread == mainJavaThread) {
			return mainHaxeThread;
		} else if(javaThread is NativeHaxeThread) {
			return (cast javaThread:NativeHaxeThread).haxeThread;
		} if(javaThread is VirtualHaxeThread) {
			return (cast javaThread:VirtualHaxeThread).haxeThread;
		} else {
			switch nativeThreads.get(javaThread) {
				case null:
					var hx = new HaxeThread();
					nativeThreads.put(javaThread, hx);
					return hx;
				case hx:
					return hx;
			}
		}
	}

	public static function runWithEventLoop(job:()->Void):Void {
		var thread = get(JavaThread.currentThread());
		if(thread.events == null) {
			thread.events = new EventLoop();
			try {
				job();
				thread.events.loop();
				thread.events = null;
			} catch(e) {
				thread.events = null;
				throw e;
			}
		} else {
			job();
		}
	}

	function new() {}

	public function sendMessage(msg:Dynamic):Void {
		messages.add(msg);
	}

	public function readMessage(block:Bool):Dynamic {
		return block ? messages.take() : messages.poll();
	}
}

private class NativeHaxeThread extends java.lang.Thread implements IJavaThread {
	public final haxeThread:HaxeThread;
	final withEventLoop:Bool;

	public function new(haxeThread:HaxeThread, job:()->Void, withEventLoop:Bool) {
		super(new Job(job));
		this.haxeThread = haxeThread;
		this.withEventLoop = withEventLoop;
	}

	override overload public function run() {
		super.run();
		if(withEventLoop)
			haxeThread.events.loop();
	}
}

private class VirtualHaxeThread implements IJavaThread {
	public final haxeThread:HaxeThread;
	final withEventLoop:Bool;
	private var javaThread:java.lang.Thread;
	private var job:()->Void;

	public function new(haxeThread:HaxeThread, job:()->Void, withEventLoop:Bool) {
		javaThread = VirtualThread.ofVirtual().unstarted(run);

		this.haxeThread = haxeThread;
		this.job = job;
		this.withEventLoop = withEventLoop;
	}

	overload public function run() {
		javaThread.run();
		job();
		if(withEventLoop)
			haxeThread.events.loop();
	}

	public function start():Void {
		javaThread.start();
	}

	@:throws("InterruptedException") public function join():Void {
		javaThread.join();
	}

	public function getState():Any {
		return javaThread.getState();
	}

	public function setDaemon(daemon:Bool):Void {
		javaThread.setDaemon(daemon);
	}
}

private interface IJavaThread {
	public function start():Void;
	public function run():Void;
	public function join():Void;
	public function setDaemon(daemon:Bool):Void;
	public function getState():Any;
}

@:native("java.lang.Thread")
extern class VirtualThread {
	// Constructor
	@:overload function new(runnable:Runnable);
	// Instance methods
	@:overload function start():Void;
	@:overload function join():Void;

	// Static methods for virtual threads (Java 19+)
	@:overload static function startVirtualThread(runnable:Runnable):Thread;
	@:overload static function ofVirtual(runnable:Runnable):Thread;
	@:overload static function ofVirtual():OfVirtual;
	@:overload static function currentThread():VirtualThread;
	@:overload function isVirtual():Bool;
}

@:native("java.lang.Thread$Builder$OfVirtual")
extern interface OfVirtual {
	public function unstarted(task:Runnable):java.lang.Thread;
}

#if jvm
private abstract Job(Runnable) from Runnable to Runnable {
	public inline function new(job:()->Void) {
		this = cast job;
	}
}
#else
private class Job implements Runnable {
	final job:()->Void;

	public function new(job:()->Void) {
		this.job = job;
	}

	public function run() {
		job();
	}
}
#end

Would appreciate any feedback or guidance on whether this direction makes sense, or how you'd prefer this kind of advanced JDK-specific support to be handled in hxjava.

The current extern approach should not break compatibility with older jvm versions.

Thanks a lot!

barisyild avatar Jul 04 '25 17:07 barisyild

Test Case

Native Thread

Image

Started 50000 threads to accept sockets. Time taken: 161.35700011253357 seconds

Virtual Thread

Image

Started 50000 threads to accept sockets. Time taken: 0.18600010871887207 seconds.

barisyild avatar Jul 04 '25 17:07 barisyild

I don't really want to mess with the existing thread API. Can't we just make this its own thing?

Simn avatar Jul 07 '25 09:07 Simn

I don't really want to mess with the existing thread API. Can't we just make this its own thing?

Are you talking about creating a different class? Like VirtualThread.create.

barisyild avatar Jul 07 '25 12:07 barisyild

Yes

Simn avatar Jul 07 '25 12:07 Simn

Yes

Don't you think this should be in the standard library?

barisyild avatar Jul 07 '25 12:07 barisyild

@barisyild Since this is a JVM-specific feature, I think it makes sense to like in the jvm package, as it's not a cross-platform API.

9Morello avatar Jul 12 '25 14:07 9Morello

Which package do you recommend I use? java.lang.VirtualThread?

barisyild avatar Jul 12 '25 22:07 barisyild

I think you could create a jvm.Thread class and start there. Later on, other people can provide feedback on your API.

But overall, I think the best way for you to go is to make an initial implementation as a haxelib, test it, maybe put some examples on a repository, etc. Then ask @Simn and the others how to go about a proprosal to make it part of the standard library.

9Morello avatar Jul 13 '25 10:07 9Morello

I think you could create a jvm.Thread class and start there. Later on, other people can provide feedback on your API.

But overall, I think the best way for you to go is to make an initial implementation as a haxelib, test it, maybe put some examples on a repository, etc. Then ask @Simn and the others how to go about a proprosal to make it part of the standard library.

It can probably be included in the standard library in this form.

When I separate it from std, Thread.current etc. will probably not work.

Actually the best option is to build hxjava with a higher java version but this may completely break the old version support.

Std way seems like the most optimal way right now.

barisyild avatar Jul 13 '25 21:07 barisyild

The following approach is much better for haxe.

@Simn

Example usage

Thread

var thread = Thread.create(() -> {
    if(!VirtualThread.isVirtualThread())
		trace("Regular Thread!");
});

Virtual Thread

var thread = VirtualThread.create(() -> {
    if(VirtualThread.isVirtualThread())
		trace("Virtual Thread!");
});

Classes

jvm.thread.VirtualThread

package jvm.thread;

import sys.thread.Thread;
import sys.thread.Thread.VirtualHaxeThread;
import java.lang.Thread as JavaThread;

class VirtualThread {
	public static inline function create(job:()->Void):Thread {
		// TODO: Check Java Version and Throw Exception
		
		return Thread.createVirtualThread(job);
	}
	
	public static inline function createWithEventLoop(job:()->Void):Thread {
		// TODO: Check Java Version and Throw Exception
		
		return Thread.createWithVirtualThreadEventLoop(job);
	}
	
	public static inline function isVirtualThread():Bool {
		return JavaThread.currentThread() is VirtualHaxeThread;
	}
}

sys.thread.Thread

package sys.thread;

import java.Lib;
import java.lang.Runnable;
import java.util.WeakHashMap;
import java.util.Collections;
import java.lang.Thread as JavaThread;
import java.lang.System;
import java.StdTypes.Int64 as Long;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.LinkedBlockingDeque;

private typedef ThreadImpl = HaxeThread;

@:allow(jvm.thread.VirtualThread)
abstract Thread(ThreadImpl) from ThreadImpl {
	public var events(get,never):EventLoop;

	inline function new(t:HaxeThread) {
		this = t;
	}

	public static inline function create(job:()->Void):Thread {
		return HaxeThread.create(job, false, false);
	}
	
	private static inline function createVirtualThread(job:()->Void):Thread {
		return HaxeThread.create(job, false, true);
	}

	public static inline function current():Thread {
		return HaxeThread.get(JavaThread.currentThread());
	}

	public static inline function runWithEventLoop(job:()->Void):Void {
		HaxeThread.runWithEventLoop(job);
	}

	public static inline function createWithEventLoop(job:()->Void):Thread {
		return HaxeThread.create(job, true, false);
	}
	
	private static inline function createWithVirtualThreadEventLoop(job:()->Void):Thread {
		return HaxeThread.create(job, true, true);
	}

	public static inline function readMessage(block:Bool):Dynamic {
		return current().getHandle().readMessage(block);
	}

	public inline function sendMessage(msg:Dynamic):Void {
		this.sendMessage(msg);
	}

	inline function getHandle():HaxeThread {
		return this;
	}

	function get_events():EventLoop {
		if(this.events == null)
			throw new NoEventLoopException();
		return this.events;
	}

	@:keep //TODO: keep only if events are actually used
	static function processEvents():Void {
		current().getHandle().events.loop();
	}
}

private class HaxeThread {
	static var nativeThreads:java.util.Map<JavaThread,HaxeThread>;
	static var mainJavaThread:JavaThread;
	static var mainHaxeThread:HaxeThread;

	static function __init__() {
		nativeThreads = Collections.synchronizedMap(new WeakHashMap<JavaThread,HaxeThread>());
		mainJavaThread = JavaThread.currentThread();
		mainHaxeThread = new HaxeThread();
		mainHaxeThread.events = new EventLoop();
	}

	public final messages = new LinkedBlockingDeque<Dynamic>();

	public var events(default,null):Null<EventLoop>;
	public var isVirtual(default, null):Bool;

	public static function create(job:()->Void, withEventLoop:Bool, isVirtual:Bool = false):HaxeThread {
		var hx = new HaxeThread();
		hx.isVirtual = isVirtual;
		if(withEventLoop)
			hx.events = new EventLoop();

		var thread:IJavaThread;
		if(isVirtual) {
			thread = new VirtualHaxeThread(hx, job, withEventLoop);
		}else{
			thread = new NativeHaxeThread(hx, job, withEventLoop);
		}

		thread.setDaemon(true);
		thread.start();

		return hx;
	}

	public static function get(javaThread:JavaThread):HaxeThread {
		if(javaThread == mainJavaThread) {
			return mainHaxeThread;
		} else if(javaThread is NativeHaxeThread) {
			return (cast javaThread:NativeHaxeThread).haxeThread;
		} if(javaThread is VirtualHaxeThread) {
			return (cast javaThread:VirtualHaxeThread).haxeThread;
		} else {
			switch nativeThreads.get(javaThread) {
				case null:
					var hx = new HaxeThread();
					nativeThreads.put(javaThread, hx);
					return hx;
				case hx:
					return hx;
			}
		}
	}

	public static function runWithEventLoop(job:()->Void):Void {
		var thread = get(JavaThread.currentThread());
		if(thread.events == null) {
			thread.events = new EventLoop();
			try {
				job();
				thread.events.loop();
				thread.events = null;
			} catch(e) {
				thread.events = null;
				throw e;
			}
		} else {
			job();
		}
	}

	function new() {}

	public function sendMessage(msg:Dynamic):Void {
		messages.add(msg);
	}

	public function readMessage(block:Bool):Dynamic {
		return block ? messages.take() : messages.poll();
	}
}

private class NativeHaxeThread extends java.lang.Thread implements IJavaThread {
	public final haxeThread:HaxeThread;
	final withEventLoop:Bool;

	public function new(haxeThread:HaxeThread, job:()->Void, withEventLoop:Bool) {
		super(new Job(job));
		this.haxeThread = haxeThread;
		this.withEventLoop = withEventLoop;
	}

	override overload public function run() {
		super.run();
		if(withEventLoop)
			haxeThread.events.loop();
	}
}

private class VirtualHaxeThread implements IJavaThread {
	public final haxeThread:HaxeThread;
	final withEventLoop:Bool;
	private var javaThread:java.lang.Thread;
	private var job:()->Void;

	public function new(haxeThread:HaxeThread, job:()->Void, withEventLoop:Bool) {
		javaThread = VirtualThread.ofVirtual().unstarted(run);

		this.haxeThread = haxeThread;
		this.job = job;
		this.withEventLoop = withEventLoop;
	}

	overload public function run() {
		javaThread.run();
		job();
		if(withEventLoop)
			haxeThread.events.loop();
	}

	public function start():Void {
		javaThread.start();
	}

	@:throws("InterruptedException") public function join():Void {
		javaThread.join();
	}

	public function getState():Any {
		return javaThread.getState();
	}

	public function setDaemon(daemon:Bool):Void {
		javaThread.setDaemon(daemon);
	}
}

private interface IJavaThread {
	public function start():Void;
	public function run():Void;
	public function join():Void;
	public function setDaemon(daemon:Bool):Void;
	public function getState():Any;
}

@:native("java.lang.Thread")
extern class VirtualThread {
	// Constructor
	@:overload function new(runnable:Runnable);
	// Instance methods
	@:overload function start():Void;
	@:overload function join():Void;

	// Static methods for virtual threads (Java 19+)
	@:overload static function startVirtualThread(runnable:Runnable):Thread;
	@:overload static function ofVirtual(runnable:Runnable):Thread;
	@:overload static function ofVirtual():OfVirtual;
	@:overload static function currentThread():VirtualThread;
	@:overload function isVirtual():Bool;
}

@:native("java.lang.Thread$Builder$OfVirtual")
extern interface OfVirtual {
	public function unstarted(task:Runnable):java.lang.Thread;
}

#if jvm
private abstract Job(Runnable) from Runnable to Runnable {
	public inline function new(job:()->Void) {
		this = cast job;
	}
}
#else
private class Job implements Runnable {
	final job:()->Void;

	public function new(job:()->Void) {
		this.job = job;
	}

	public function run() {
		job();
	}
}
#end

barisyild avatar Nov 04 '25 16:11 barisyild