Files
leonwww/flumi/Scripts/Utils/Lua/ThreadedVM.gd
2025-08-23 15:47:18 +03:00

567 lines
16 KiB
GDScript

class_name ThreadedLuaVM
extends RefCounted
signal script_completed(result: Dictionary)
signal script_error(error: String)
signal print_output(message: String)
signal dom_operation_request(operation: Dictionary)
var lua_thread: Thread
var lua_vm: LuauVM
var lua_api: LuaAPI
var dom_parser: HTMLParser
var command_queue: Array = []
var queue_mutex: Mutex
var should_exit: bool = false
var thread_semaphore: Semaphore
# Sleep system
var sleep_mutex: Mutex
var sleep_condition: bool = false
var sleep_end_time: float = 0.0
func _init():
queue_mutex = Mutex.new()
sleep_mutex = Mutex.new()
thread_semaphore = Semaphore.new()
func start_lua_thread(dom_parser_ref: HTMLParser, lua_api_ref: LuaAPI) -> bool:
if lua_thread and lua_thread.is_alive():
return false
dom_parser = dom_parser_ref
lua_api = lua_api_ref
should_exit = false
lua_thread = Thread.new()
var error = lua_thread.start(_lua_thread_worker)
if error != OK:
return false
return true
func stop_lua_thread():
if not lua_thread or not lua_thread.is_alive():
return
should_exit = true
thread_semaphore.post() # Wake up BRO
# short time to exit gracefully
var timeout_start = Time.get_ticks_msec()
while lua_thread.is_alive() and (Time.get_ticks_msec() - timeout_start) < 500:
OS.delay_msec(10)
lua_thread = null
func execute_script_async(script_code: String):
queue_mutex.lock()
command_queue.append({
"type": "execute_script",
"code": script_code
})
queue_mutex.unlock()
thread_semaphore.post()
func execute_callback_async(callback_ref: int, args: Array = []):
if not lua_thread or not lua_thread.is_alive():
return
queue_mutex.lock()
command_queue.append({
"type": "execute_callback",
"callback_ref": callback_ref,
"args": args
})
queue_mutex.unlock()
thread_semaphore.post()
func execute_timeout_callback_async(timeout_id: int):
if not lua_thread or not lua_thread.is_alive():
return
queue_mutex.lock()
command_queue.append({
"type": "execute_timeout",
"timeout_id": timeout_id
})
queue_mutex.unlock()
thread_semaphore.post()
func sleep_lua(duration_seconds: float):
sleep_mutex.lock()
sleep_end_time = Time.get_ticks_msec() / 1000.0 + duration_seconds
sleep_condition = true
sleep_mutex.unlock()
while true:
sleep_mutex.lock()
var current_time = Time.get_ticks_msec() / 1000.0
var should_continue = sleep_condition and current_time < sleep_end_time
sleep_mutex.unlock()
if not should_continue:
break
# Yield to allow other threads to run
OS.delay_msec(1)
func _lua_thread_worker():
lua_vm = LuauVM.new()
lua_vm.open_libraries([lua_vm.LUA_BASE_LIB, lua_vm.LUA_BIT32_LIB,
lua_vm.LUA_COROUTINE_LIB, lua_vm.LUA_MATH_LIB, lua_vm.LUA_UTF8_LIB,
lua_vm.LUA_TABLE_LIB, lua_vm.LUA_STRING_LIB, lua_vm.LUA_VECTOR_LIB])
lua_vm.lua_pushcallable(_print_handler, "print")
lua_vm.lua_setglobal("print")
# Setup threaded Time.sleep function
lua_vm.lua_newtable()
lua_vm.lua_pushcallable(_time_sleep_handler, "Time.sleep")
lua_vm.lua_setfield(-2, "sleep")
lua_vm.lua_setglobal("Time")
_setup_threaded_gurt_api()
_setup_additional_lua_apis()
while not should_exit:
if thread_semaphore.try_wait():
_process_command_queue()
else:
OS.delay_msec(10)
lua_vm = null
func _process_command_queue():
queue_mutex.lock()
var commands_to_process = command_queue.duplicate()
command_queue.clear()
queue_mutex.unlock()
for command in commands_to_process:
match command.type:
"execute_script":
_execute_script_in_thread(command.code)
"execute_callback":
_execute_callback_in_thread(command.callback_ref, command.args)
"execute_timeout":
_execute_timeout_in_thread(command.timeout_id)
func _execute_script_in_thread(script_code: String):
if not lua_vm:
call_deferred("_emit_script_error", "Lua VM not initialized")
return
var result = lua_vm.lua_dostring(script_code)
if result == lua_vm.LUA_OK:
call_deferred("_emit_script_completed", {"success": true})
else:
var error_msg = lua_vm.lua_tostring(-1)
lua_vm.lua_pop(1)
call_deferred("_emit_script_error", error_msg)
func _call_lua_function_with_args(args: Array) -> bool:
# Push arguments
for arg in args:
lua_vm.lua_pushvariant(arg)
# Execute the callback with proper error handling
if lua_vm.lua_pcall(args.size(), 0, 0) != lua_vm.LUA_OK:
var error_msg = lua_vm.lua_tostring(-1)
lua_vm.lua_pop(1)
call_deferred("_emit_script_error", "Callback error: " + error_msg)
return false
return true
func _execute_callback_in_thread(callback_ref: int, args: Array):
if not lua_vm:
return
lua_vm.lua_pushstring("THREADED_CALLBACKS")
lua_vm.lua_rawget(lua_vm.LUA_REGISTRYINDEX)
if not lua_vm.lua_isnil(-1):
lua_vm.lua_pushinteger(callback_ref)
lua_vm.lua_rawget(-2)
if lua_vm.lua_isfunction(-1):
lua_vm.lua_remove(-2) # Remove the table, keep the function
if _call_lua_function_with_args(args):
return
else:
lua_vm.lua_pop(1) # Pop non-function value
lua_vm.lua_pop(1) # Pop the table
# Fallback to regular registry lookup
lua_vm.lua_rawgeti(lua_vm.LUA_REGISTRYINDEX, callback_ref)
if lua_vm.lua_isfunction(-1):
_call_lua_function_with_args(args)
else:
lua_vm.lua_pop(1)
func _execute_timeout_in_thread(timeout_id: int):
if not lua_vm:
return
# Check if this is an interval by looking at the timeout manager
var timeout_info = lua_api.timeout_manager.active_timeouts.get(timeout_id, null)
var is_interval = timeout_info != null and timeout_info.is_interval
# Retrieve timeout callback from the special timeout registry
lua_vm.lua_pushstring("GURT_THREADED_TIMEOUTS")
lua_vm.lua_rawget(lua_vm.LUA_REGISTRYINDEX)
if not lua_vm.lua_isnil(-1):
lua_vm.lua_pushinteger(timeout_id)
lua_vm.lua_rawget(-2)
if lua_vm.lua_isfunction(-1):
lua_vm.lua_remove(-2) # Remove the table, keep the function
if _call_lua_function_with_args([]):
# Only clean up the callback if it's a timeout (not an interval)
if not is_interval:
lua_vm.lua_pushstring("GURT_THREADED_TIMEOUTS")
lua_vm.lua_rawget(lua_vm.LUA_REGISTRYINDEX)
if not lua_vm.lua_isnil(-1):
lua_vm.lua_pushinteger(timeout_id)
lua_vm.lua_pushnil()
lua_vm.lua_rawset(-3)
lua_vm.lua_pop(1)
return
else:
lua_vm.lua_pop(1) # Pop non-function value
lua_vm.lua_pop(1) # Pop the table
func _print_handler(vm: LuauVM) -> int:
var message_parts: Array = []
var num_args = vm.lua_gettop()
for i in range(1, num_args + 1):
var arg_str = ""
if vm.lua_isstring(i):
arg_str = vm.lua_tostring(i)
elif vm.lua_isnumber(i):
arg_str = str(vm.lua_tonumber(i))
elif vm.lua_isboolean(i):
arg_str = "true" if vm.lua_toboolean(i) else "false"
elif vm.lua_isnil(i):
arg_str = "nil"
else:
arg_str = vm.lua_typename(vm.lua_type(i))
message_parts.append(arg_str)
var final_message = "\t".join(message_parts)
var current_time = Time.get_ticks_msec() / 1000.0
call_deferred("_emit_print_output", final_message)
return 0
func _time_sleep_handler(vm: LuauVM) -> int:
vm.luaL_checknumber(1)
var seconds = vm.lua_tonumber(1)
if seconds > 0:
sleep_lua(seconds)
return 0
func _setup_threaded_gurt_api():
lua_vm.lua_pushcallable(_print_handler, "print")
lua_vm.lua_setglobal("print")
LuaTimeUtils.setup_time_api(lua_vm)
lua_vm.lua_getglobal("Time")
if not lua_vm.lua_isnil(-1):
lua_vm.lua_pushcallable(_time_sleep_handler, "Time.sleep")
lua_vm.lua_setfield(-2, "sleep")
lua_vm.lua_pop(1)
lua_vm.lua_newtable()
lua_vm.lua_pushcallable(_print_handler, "gurt.log")
lua_vm.lua_setfield(-2, "log")
lua_vm.lua_pushcallable(_gurt_select_handler, "gurt.select")
lua_vm.lua_setfield(-2, "select")
lua_vm.lua_pushcallable(_gurt_select_all_handler, "gurt.selectAll")
lua_vm.lua_setfield(-2, "selectAll")
lua_vm.lua_pushcallable(_gurt_create_handler, "gurt.create")
lua_vm.lua_setfield(-2, "create")
# Add timer functions as global functions
lua_vm.lua_pushcallable(_set_timeout_handler, "setTimeout")
lua_vm.lua_setglobal("setTimeout")
lua_vm.lua_pushcallable(lua_api._gurt_clear_timeout_handler, "clearTimeout")
lua_vm.lua_setglobal("clearTimeout")
lua_vm.lua_pushcallable(_set_interval_handler, "setInterval")
lua_vm.lua_setglobal("setInterval")
lua_vm.lua_pushcallable(lua_api._gurt_clear_interval_handler, "clearInterval")
lua_vm.lua_setglobal("clearInterval")
lua_vm.lua_newtable()
lua_vm.lua_pushcallable(lua_api._gurt_location_reload_handler, "gurt.location.reload")
lua_vm.lua_setfield(-2, "reload")
lua_vm.lua_pushcallable(lua_api._gurt_location_goto_handler, "gurt.location.goto")
lua_vm.lua_setfield(-2, "goto")
var current_href = get_current_href()
lua_vm.lua_pushstring(current_href)
lua_vm.lua_setfield(-2, "href")
lua_vm.lua_newtable()
lua_vm.lua_pushcallable(lua_api._gurt_location_query_get_handler, "gurt.location.query.get")
lua_vm.lua_setfield(-2, "get")
lua_vm.lua_pushcallable(lua_api._gurt_location_query_has_handler, "gurt.location.query.has")
lua_vm.lua_setfield(-2, "has")
lua_vm.lua_pushcallable(lua_api._gurt_location_query_getAll_handler, "gurt.location.query.getAll")
lua_vm.lua_setfield(-2, "getAll")
lua_vm.lua_setfield(-2, "query")
lua_vm.lua_setfield(-2, "location")
var body_element = dom_parser.find_first("body")
if body_element:
LuaDOMUtils.create_element_wrapper(lua_vm, body_element, lua_api)
lua_vm.lua_pushcallable(_body_on_handler, "body.on")
lua_vm.lua_setfield(-2, "on")
lua_vm.lua_setfield(-2, "body")
lua_vm.lua_setglobal("gurt")
func _setup_additional_lua_apis():
lua_vm.lua_getglobal("table")
if lua_vm.lua_isnil(-1):
lua_vm.lua_pop(1)
lua_vm.lua_newtable()
lua_vm.lua_setglobal("table")
lua_vm.lua_getglobal("table")
lua_vm.lua_pushcallable(_table_tostring_handler, "table.tostring")
lua_vm.lua_setfield(-2, "tostring")
lua_vm.lua_pop(1)
LuaSignalUtils.setup_signal_api(lua_vm)
LuaClipboardUtils.setup_clipboard_api(lua_vm)
LuaNetworkUtils.setup_network_api(lua_vm)
LuaJSONUtils.setup_json_api(lua_vm)
LuaWebSocketUtils.setup_websocket_api(lua_vm)
LuaAudioUtils.setup_audio_api(lua_vm)
LuaCrumbsUtils.setup_crumbs_api(lua_vm)
LuaRegexUtils.setup_regex_api(lua_vm)
func _table_tostring_handler(vm: LuauVM) -> int:
vm.luaL_checktype(1, vm.LUA_TTABLE)
var table_string = LuaPrintUtils.table_to_string(vm, 1)
vm.lua_pushstring(table_string)
return 1
func _emit_script_completed(result: Dictionary):
script_completed.emit(result)
func _emit_script_error(error: String):
script_error.emit(error)
func _emit_print_output(message: String):
print_output.emit(message)
func _gurt_select_all_handler(vm: LuauVM) -> int:
var selector: String = vm.luaL_checkstring(1)
if not dom_parser or not dom_parser.parse_result:
vm.lua_newtable()
return 1
var elements = SelectorUtils.find_all_matching(selector, dom_parser.parse_result.all_elements)
vm.lua_newtable()
for i in range(elements.size()):
vm.lua_pushinteger(i + 1)
LuaDOMUtils.create_element_wrapper(vm, elements[i], lua_api)
vm.lua_rawset(-3)
return 1
func _gurt_create_handler(vm: LuauVM) -> int:
var tag_name: String = vm.luaL_checkstring(1)
var attributes = {}
if vm.lua_gettop() >= 2 and not vm.lua_isnil(2):
vm.luaL_checktype(2, vm.LUA_TTABLE)
attributes = vm.lua_todictionary(2)
var new_element = HTMLParser.HTMLElement.new(tag_name)
for attr_name in attributes:
if attr_name == "text":
new_element.text_content = str(attributes[attr_name])
else:
new_element.set_attribute(attr_name, str(attributes[attr_name]))
var element_id = lua_api.get_or_assign_element_id(new_element)
new_element.set_attribute("id", element_id)
dom_parser.parse_result.all_elements.append(new_element)
LuaDOMUtils.create_element_wrapper(vm, new_element, lua_api)
return 1
func _set_timeout_handler(vm: LuauVM) -> int:
vm.luaL_checktype(1, vm.LUA_TFUNCTION)
var delay_ms: int = vm.luaL_checkint(2)
var timeout_id = lua_api.timeout_manager.next_timeout_id
lua_api.timeout_manager.next_timeout_id += 1
vm.lua_pushstring("GURT_THREADED_TIMEOUTS")
vm.lua_rawget(vm.LUA_REGISTRYINDEX)
if vm.lua_isnil(-1):
vm.lua_pop(1)
vm.lua_newtable()
vm.lua_pushstring("GURT_THREADED_TIMEOUTS")
vm.lua_pushvalue(-2)
vm.lua_rawset(vm.LUA_REGISTRYINDEX)
vm.lua_pushinteger(timeout_id)
vm.lua_pushvalue(1)
vm.lua_rawset(-3)
vm.lua_pop(1)
call_deferred("_create_threaded_timeout", timeout_id, delay_ms)
vm.lua_pushinteger(timeout_id)
return 1
func _clear_timeout_handler(vm: LuauVM) -> int:
return lua_api._gurt_clear_timeout_handler(vm)
func _set_interval_handler(vm: LuauVM) -> int:
vm.luaL_checktype(1, vm.LUA_TFUNCTION)
var delay_ms: int = vm.luaL_checkint(2)
var interval_id = lua_api.timeout_manager.next_timeout_id
lua_api.timeout_manager.next_timeout_id += 1
vm.lua_pushstring("GURT_THREADED_TIMEOUTS")
vm.lua_rawget(vm.LUA_REGISTRYINDEX)
if vm.lua_isnil(-1):
vm.lua_pop(1)
vm.lua_newtable()
vm.lua_pushstring("GURT_THREADED_TIMEOUTS")
vm.lua_pushvalue(-2)
vm.lua_rawset(vm.LUA_REGISTRYINDEX)
vm.lua_pushinteger(interval_id)
vm.lua_pushvalue(1)
vm.lua_rawset(-3)
vm.lua_pop(1)
call_deferred("_create_threaded_interval", interval_id, delay_ms)
vm.lua_pushinteger(interval_id)
return 1
func get_current_href() -> String:
var main_node = Engine.get_main_loop().current_scene
return main_node.current_domain
func _gurt_select_handler(vm: LuauVM) -> int:
var selector: String = vm.luaL_checkstring(1)
if not dom_parser or not dom_parser.parse_result:
vm.lua_pushnil()
return 1
var element = SelectorUtils.find_first_matching(selector, dom_parser.parse_result.all_elements)
if element:
LuaDOMUtils.create_element_wrapper(vm, element, lua_api)
return 1
else:
vm.lua_pushnil()
return 1
func _body_on_handler(vm: LuauVM) -> int:
vm.luaL_checktype(1, vm.LUA_TTABLE)
var event_name: String = vm.luaL_checkstring(2)
vm.luaL_checktype(3, vm.LUA_TFUNCTION)
vm.lua_pushstring("THREADED_CALLBACKS")
vm.lua_rawget(vm.LUA_REGISTRYINDEX)
if vm.lua_isnil(-1):
vm.lua_pop(1)
vm.lua_newtable()
vm.lua_pushstring("THREADED_CALLBACKS")
vm.lua_pushvalue(-2)
vm.lua_rawset(vm.LUA_REGISTRYINDEX)
var callback_ref = lua_api.next_callback_ref
lua_api.next_callback_ref += 1
var subscription_id = lua_api.next_subscription_id
lua_api.next_subscription_id += 1
vm.lua_pushinteger(callback_ref)
vm.lua_pushvalue(3)
vm.lua_rawset(-3)
vm.lua_pop(1)
var operation = {
"type": "register_body_event",
"event_name": event_name,
"callback_ref": callback_ref,
"subscription_id": subscription_id
}
call_deferred("_emit_dom_operation_request", operation)
vm.lua_newtable()
vm.lua_pushinteger(subscription_id)
vm.lua_setfield(-2, "_subscription_id")
vm.lua_pushcallable(LuaDOMUtils._unsubscribe_wrapper, "subscription.unsubscribe")
vm.lua_setfield(-2, "unsubscribe")
return 1
func _emit_dom_operation_request(operation: Dictionary):
dom_operation_request.emit(operation)
func _create_threaded_timeout(timeout_id: int, delay_ms: int):
lua_api._ensure_timeout_manager()
var timeout_info = lua_api.timeout_manager.TimeoutInfo.new(timeout_id, timeout_id, lua_vm, lua_api.timeout_manager, false, delay_ms)
lua_api.timeout_manager.active_timeouts[timeout_id] = timeout_info
lua_api.timeout_manager.threaded_vm = self
var timer = Timer.new()
timer.wait_time = delay_ms / 1000.0
timer.one_shot = true
timer.timeout.connect(lua_api.timeout_manager._on_timeout_triggered.bind(timeout_info))
timeout_info.timer = timer
lua_api.add_child(timer)
timer.start()
func _create_threaded_interval(interval_id: int, delay_ms: int):
lua_api._ensure_timeout_manager()
var timeout_info = lua_api.timeout_manager.TimeoutInfo.new(interval_id, interval_id, lua_vm, lua_api.timeout_manager, true, delay_ms)
lua_api.timeout_manager.active_timeouts[interval_id] = timeout_info
lua_api.timeout_manager.threaded_vm = self
var timer = Timer.new()
timer.wait_time = delay_ms / 1000.0
timer.one_shot = false
timer.timeout.connect(lua_api.timeout_manager._on_timeout_triggered.bind(timeout_info))
timeout_info.timer = timer
lua_api.add_child(timer)
timer.start()