Introduce the concept of tasks in the engine

This commit is contained in:
Martín Capello 2025-02-13 15:01:27 -03:00
parent be6d2251aa
commit bbf94cfab0
5 changed files with 389 additions and 89 deletions

View File

@ -73,7 +73,7 @@ void RunScriptCommand::onExecute(Context* context)
return;
}
App::instance()->scriptEngine()->evalUserFile(m_filename, m_params);
App::instance()->scriptEngine()->evalUserFileInTask(m_filename, m_params);
if (context->isUIAvailable())
ui::Manager::getDefault()->invalidate();

View File

@ -32,6 +32,7 @@
#include "base/fs.h"
#include "base/paths.h"
#include "base/remove_from_container.h"
#include "ui/await.h"
#include "ui/box.h"
#include "ui/button.h"
#include "ui/combobox.h"
@ -125,6 +126,7 @@ struct Dialog {
// Reference used to keep the dialog alive (so it's not garbage
// collected) when it's visible.
int showRef = LUA_REFNIL;
int dlgStateRef = LUA_REFNIL;
lua_State* L = nullptr;
Dialog(const ui::Window::Type windowType, const std::string& title)
@ -152,6 +154,11 @@ struct Dialog {
this->L = L;
lua_pushvalue(L, 1);
showRef = luaL_ref(L, LUA_REGISTRYINDEX);
// Keep a reference of this thread state while the dialog is still alive
// because it will be reused when calling the event handlers to create
// new lua threads.
lua_pushthread(L);
dlgStateRef = luaL_ref(L, LUA_REGISTRYINDEX);
}
}
@ -163,8 +170,14 @@ struct Dialog {
void unrefShow()
{
if (showRef != LUA_REFNIL) {
int status = lua_status(this->L);
TRACE("status: %d %d\n", showRef, status);
// this->L
luaL_unref(this->L, LUA_REGISTRYINDEX, showRef);
luaL_unref(this->L, LUA_REGISTRYINDEX, dlgStateRef);
showRef = LUA_REFNIL;
dlgStateRef = LUA_REFNIL;
L = nullptr;
}
}
@ -336,18 +349,7 @@ void Dialog_connect_signal(lua_State* L,
callback(L, std::forward<Args>(args)...);
if (lua_isfunction(L, -2)) {
try {
if (lua_pcall(L, 1, 0, 0)) {
if (const char* s = lua_tostring(L, -1))
App::instance()->scriptEngine()->consolePrint(s);
}
}
catch (const std::exception& ex) {
// This is used to catch unhandled exception or for
// example, std::runtime_error exceptions when a Tx() is
// created without an active sprite.
App::instance()->scriptEngine()->handleException(ex);
}
App::instance()->scriptEngine()->callInTask(L, 1);
}
else {
lua_pop(L, 1); // Pop the value which should have been a function
@ -358,63 +360,65 @@ void Dialog_connect_signal(lua_State* L,
int Dialog_new(lua_State* L)
{
// If we don't have UI, just return nil
if (!App::instance()->isGui())
return 0;
return ui::await<int>([L]() -> int {
// If we don't have UI, just return nil
if (!App::instance()->isGui())
return 0;
// Get the title and the type of window (with or without title bar)
ui::Window::Type windowType = ui::Window::WithTitleBar;
std::string title = "Script";
if (lua_isstring(L, 1)) {
title = lua_tostring(L, 1);
}
else if (lua_istable(L, 1)) {
int type = lua_getfield(L, 1, "title");
if (type != LUA_TNIL)
title = lua_tostring(L, -1);
lua_pop(L, 1);
type = lua_getfield(L, 1, "notitlebar");
if (type != LUA_TNIL && lua_toboolean(L, -1))
windowType = ui::Window::WithoutTitleBar;
lua_pop(L, 1);
}
auto dlg = push_new<Dialog>(L, windowType, title);
// The uservalue of the dialog userdata will contain a table that
// stores all the callbacks to handle events. As these callbacks can
// reference the dialog itself, it's important to store callbacks in
// this table that depends on the dialog lifetime itself
// (i.e. uservalue) and in the global registry, because in that case
// we could create a cyclic reference that would be not GC'd.
lua_newtable(L);
lua_setuservalue(L, -2);
if (lua_istable(L, 1)) {
int type = lua_getfield(L, 1, "parent");
if (type != LUA_TNIL) {
if (auto parentDlg = may_get_obj<Dialog>(L, -1))
dlg->window.setParentDisplay(parentDlg->window.display());
// Get the title and the type of window (with or without title bar)
ui::Window::Type windowType = ui::Window::WithTitleBar;
std::string title = "Script";
if (lua_isstring(L, 1)) {
title = lua_tostring(L, 1);
}
lua_pop(L, 1);
else if (lua_istable(L, 1)) {
int type = lua_getfield(L, 1, "title");
if (type != LUA_TNIL)
title = lua_tostring(L, -1);
lua_pop(L, 1);
type = lua_getfield(L, 1, "onclose");
if (type == LUA_TFUNCTION) {
Dialog_connect_signal(L, -2, dlg->window.Close, [](lua_State*, CloseEvent&) {
// Do nothing
});
type = lua_getfield(L, 1, "notitlebar");
if (type != LUA_TNIL && lua_toboolean(L, -1))
windowType = ui::Window::WithoutTitleBar;
lua_pop(L, 1);
}
lua_pop(L, 1);
}
// The showRef must be the last reference to the dialog to be
// unreferenced after the window is closed (that's why this is the
// last connection to ui::Window::Close)
dlg->unrefShowOnClose();
auto dlg = push_new<Dialog>(L, windowType, title);
TRACE_DIALOG("Dialog_new", dlg);
return 1;
// The uservalue of the dialog userdata will contain a table that
// stores all the callbacks to handle events. As these callbacks can
// reference the dialog itself, it's important to store callbacks in
// this table that depends on the dialog lifetime itself
// (i.e. uservalue) and in the global registry, because in that case
// we could create a cyclic reference that would be not GC'd.
lua_newtable(L);
lua_setuservalue(L, -2);
if (lua_istable(L, 1)) {
int type = lua_getfield(L, 1, "parent");
if (type != LUA_TNIL) {
if (auto parentDlg = may_get_obj<Dialog>(L, -1))
dlg->window.setParentDisplay(parentDlg->window.display());
}
lua_pop(L, 1);
type = lua_getfield(L, 1, "onclose");
if (type == LUA_TFUNCTION) {
Dialog_connect_signal(L, -2, dlg->window.Close, [](lua_State*, CloseEvent&) {
// Do nothing
});
}
lua_pop(L, 1);
}
// The showRef must be the last reference to the dialog to be
// unreferenced after the window is closed (that's why this is the
// last connection to ui::Window::Close)
dlg->unrefShowOnClose();
TRACE_DIALOG("Dialog_new", dlg);
return 1;
});
}
int Dialog_gc(lua_State* L)
@ -1879,31 +1883,34 @@ int Dialog_set_bounds(lua_State* L)
return 0;
}
#define wrap(func) \
[](lua_State* L) -> int { return ui::await<int>([L]() -> int { return func(L); }); }
const luaL_Reg Dialog_methods[] = {
{ "__gc", Dialog_gc },
{ "show", Dialog_show },
{ "showMenu", Dialog_showMenu },
{ "close", Dialog_close },
{ "newrow", Dialog_newrow },
{ "separator", Dialog_separator },
{ "label", Dialog_label },
{ "button", Dialog_button },
{ "check", Dialog_check },
{ "radio", Dialog_radio },
{ "menuItem", Dialog_menuItem },
{ "entry", Dialog_entry },
{ "number", Dialog_number },
{ "slider", Dialog_slider },
{ "combobox", Dialog_combobox },
{ "color", Dialog_color },
{ "shades", Dialog_shades },
{ "file", Dialog_file },
{ "canvas", Dialog_canvas },
{ "tab", Dialog_tab },
{ "endtabs", Dialog_endtabs },
{ "modify", Dialog_modify },
{ "repaint", Dialog_repaint },
{ nullptr, nullptr }
{ "__gc", wrap(Dialog_gc) },
{ "show", wrap(Dialog_show) },
{ "showMenu", Dialog_showMenu },
{ "close", Dialog_close },
{ "newrow", Dialog_newrow },
{ "separator", Dialog_separator },
{ "label", Dialog_label },
{ "button", wrap(Dialog_button) },
{ "check", Dialog_check },
{ "radio", Dialog_radio },
{ "menuItem", Dialog_menuItem },
{ "entry", Dialog_entry },
{ "number", Dialog_number },
{ "slider", Dialog_slider },
{ "combobox", Dialog_combobox },
{ "color", Dialog_color },
{ "shades", Dialog_shades },
{ "file", Dialog_file },
{ "canvas", Dialog_canvas },
{ "tab", Dialog_tab },
{ "endtabs", Dialog_endtabs },
{ "modify", Dialog_modify },
{ "repaint", Dialog_repaint },
{ nullptr, nullptr }
};
const Property Dialog_properties[] = {

View File

@ -35,6 +35,8 @@
#include "fmt/format.h"
#include "ui/base.h"
#include "ui/cursor_type.h"
#include "ui/manager.h"
#include "ui/message_loop.h"
#include "ui/mouse_button.h"
#include <fstream>
@ -213,7 +215,80 @@ void register_websocket_class(lua_State* L);
void set_app_params(lua_State* L, const Params& params);
Engine::Engine() : L(luaL_newstate()), m_delegate(nullptr), m_printLastResult(false)
#define RUNNING_TASKS "RunningTasks"
RunScriptTask::RunScriptTask(lua_State* L, int nelems, Func&& func)
{
m_mainL = L;
m_L = lua_newthread(m_mainL);
// Save a reference to the lua thread in the registry to avoid garbage collection
// before it gets executed.
m_LRef = luaL_ref(m_mainL, LUA_REGISTRYINDEX);
// Move nelems stack elements from main thread to the child lua thread.
lua_xmove(m_mainL, m_L, nelems);
// We use a global table to have access to the state of all the currently
// running scripts
int type = lua_getglobal(m_L, RUNNING_TASKS);
if (type == LUA_TNIL) {
lua_newtable(m_L);
lua_pushvalue(m_L, -1);
lua_setglobal(m_L, RUNNING_TASKS);
}
lua_pushlightuserdata(m_L, m_L);
lua_pushlightuserdata(m_L, this);
lua_settable(m_L, -3);
lua_pop(m_L, 1);
lua_sethook(
m_L,
[](lua_State* L, lua_Debug* ar) {
if (ar->event == LUA_HOOKCOUNT) {
int type = lua_getglobal(L, RUNNING_TASKS);
if (type == LUA_TTABLE) {
lua_pushlightuserdata(L, L);
lua_gettable(L, -2);
RunScriptTask* task = (RunScriptTask*)lua_topointer(L, -1);
lua_pop(L, 2);
if (task->wantsToStop()) {
lua_pushliteral(L, "Script stopped");
lua_error(L);
}
}
}
},
LUA_MASKCOUNT,
10);
// TODO: use the token for allowing the script to report progress somehow.
m_task.on_execute([this, func](base::task_token& token) { func(m_L); });
}
RunScriptTask::~RunScriptTask()
{
luaL_unref(m_mainL, LUA_REGISTRYINDEX, m_LRef);
// Collect script garbage.
lua_gc(m_L, LUA_GCCOLLECT);
}
void RunScriptTask::execute(base::thread_pool& pool)
{
m_task.start(pool);
}
void RunScriptTask::stop()
{
m_wantsToStop = true;
}
Engine::Engine()
: L(luaL_newstate())
, m_delegate(nullptr)
, m_threadPool(3)
, m_printLastResult(false)
{
#if _DEBUG
int top = lua_gettop(L);
@ -525,6 +600,12 @@ Engine::~Engine()
void Engine::destroy()
{
// Stop all running tasks.
for (auto& task : m_tasks) {
task->stop();
}
m_threadPool.wait_all();
close_all_dialogs();
lua_close(L);
L = nullptr;
@ -594,6 +675,11 @@ bool Engine::evalCode(const std::string& code, const std::string& filename)
}
void Engine::handleException(const std::exception& ex)
{
handleException(L, ex);
}
void Engine::handleException(lua_State* L, const std::exception& ex)
{
luaL_where(L, 1);
const char* where = lua_tostring(L, -1);
@ -631,6 +717,110 @@ bool Engine::evalFile(const std::string& filename, const Params& params)
return result;
}
void Engine::callInTask(lua_State* parentL, int nargs)
{
executeTask(parentL, nargs + 1, [this, nargs](lua_State* L) {
try {
if (lua_pcall(L, nargs, 0, 0)) {
if (const char* s = lua_tostring(L, -1))
consolePrint(s);
}
}
catch (const std::exception& ex) {
// This is used to catch unhandled exception or for
// example, std::runtime_error exceptions when a Tx() is
// created without an active sprite.
handleException(L, ex);
}
});
}
void Engine::executeTask(lua_State* parentL, int nelems, RunScriptTask::Func&& func)
{
auto task = std::make_unique<RunScriptTask>(parentL, nelems, std::move(func));
auto* taskPtr = task.get();
task->onDone([this, taskPtr](base::task_token&) { onTaskDone(taskPtr); });
m_tasks.push_back(std::move(task));
taskPtr->execute(m_threadPool);
}
void Engine::onTaskDone(const RunScriptTask* task)
{
for (auto it = m_tasks.begin(); it != m_tasks.end(); ++it) {
if ((*it).get() == task) {
m_tasks.erase(it);
break;
}
}
}
bool Engine::evalUserFileInTask(const std::string& filename, const Params& params)
{
executeTask(L, 0, [filename, params, this](lua_State* L) {
std::string absFilename = base::get_absolute_path(filename);
// Set the _SCRIPT_PATH global so require() can find .lua files from
// the script path.
std::string path = base::get_file_path(absFilename);
SetScriptForRequire setScript(L, path.c_str());
bool ok = true;
std::stringstream buf;
{
std::ifstream s(FSTREAM_PATH(filename));
// Returns false if we cannot open the file
if (!s) {
ok = false;
return;
}
buf << s.rdbuf();
}
AddScriptFilename addScript(absFilename);
set_app_params(L, params);
const std::string& code = buf.str();
const std::string& atFilename = "@" + absFilename;
int returnCode;
try {
if (luaL_loadbuffer(L, code.c_str(), code.size(), atFilename.c_str()) ||
lua_pcall(L, 0, 1, 0)) {
const char* s = lua_tostring(L, -1);
if (s)
TRACE("Error: %s\n", s);
// onConsoleError(s);
ok = false;
returnCode = -1;
}
else {
// Return code
if (lua_isinteger(L, -1))
returnCode = lua_tointeger(L, -1);
else
returnCode = 0;
// Code was executed correctly
if (m_printLastResult) {
if (!lua_isnone(L, -1)) {
const char* result = lua_tostring(L, -1);
if (result)
TRACE("Result: %s\n", result);
// onConsolePrint(result);
}
}
}
lua_pop(L, 1);
}
catch (const std::exception& ex) {
// handleException(ex);
TRACE("Exception: %s\n", ex.what());
ok = false;
returnCode = -1;
}
});
return true;
}
bool Engine::evalUserFile(const std::string& filename, const Params& params)
{
// Set the _SCRIPT_PATH global so require() can find .lua files from
@ -660,6 +850,12 @@ void Engine::stopDebugger()
lua_sethook(L, nullptr, 0, 0);
}
void Engine::stopScript()
{
lua_pushliteral(L, "Script stopped");
lua_error(L);
}
void Engine::onConsoleError(const char* text)
{
if (text && m_delegate)

View File

@ -16,6 +16,8 @@
#include "app/color.h"
#include "app/commands/params.h"
#include "app/extensions.h"
#include "base/task.h"
#include "base/thread_pool.h"
#include "base/uuid.h"
#include "doc/brush.h"
#include "doc/frame.h"
@ -84,8 +86,32 @@ public:
virtual void endFile(const std::string& file) = 0;
};
class RunScriptTask {
public:
typedef std::function<void(lua_State* L)> Func;
RunScriptTask(lua_State* L, int nelems, Func&& func);
~RunScriptTask();
void onDone(base::task::func_t&& funcDone) { m_task.on_done(std::move(funcDone)); }
void execute(base::thread_pool& pool);
void stop();
bool wantsToStop() const { return m_wantsToStop; }
private:
// Lua's main thread state.
lua_State* m_mainL;
// Lua's thread state based on m_mainL.
lua_State* m_L;
int m_LRef = LUA_REFNIL;
base::task m_task;
bool m_wantsToStop = false;
};
class Engine {
public:
typedef std::vector<std::unique_ptr<RunScriptTask>> Tasks;
Engine();
~Engine();
@ -101,8 +127,13 @@ public:
bool evalCode(const std::string& code, const std::string& filename = std::string());
bool evalFile(const std::string& filename, const Params& params = Params());
bool evalUserFile(const std::string& filename, const Params& params = Params());
bool evalUserFileInTask(const std::string& filename, const Params& params = Params());
// Calls the function in the stack with the number of arguments specified by nargs in
// a new RunScriptTask.
void callInTask(lua_State* L, int nargs);
void handleException(const std::exception& ex);
void handleException(lua_State* L, const std::exception& ex);
void consolePrint(const char* text) { onConsolePrint(text); }
@ -112,13 +143,22 @@ public:
void startDebugger(DebuggerDelegate* debuggerDelegate);
void stopDebugger();
// Stops the currently running lua chunk
void stopScript();
private:
void onConsoleError(const char* text);
void onConsolePrint(const char* text);
// Creates a new RunScriptTask based on parentL and moving nelems from parentL's stack
// to the child lua thread stack
void executeTask(lua_State* parentL, int nelems, RunScriptTask::Func&& func);
void onTaskDone(const RunScriptTask* task);
static void checkProgress(lua_State* L, lua_Debug* ar);
lua_State* L;
EngineDelegate* m_delegate;
base::thread_pool m_threadPool;
Tasks m_tasks;
bool m_printLastResult;
int m_returnCode;
};

57
src/ui/await.h Normal file
View File

@ -0,0 +1,57 @@
// Aseprite UI Library
// Copyright (C) 2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef UI_AWAIT_H_INCLUDED
#define UI_AWAIT_H_INCLUDED
#include <type_traits>
#pragma once
#include "os/event.h"
#include "os/event_queue.h"
#include "ui/system.h"
#include <functional>
#include <future>
namespace ui {
// Awaits for the future result of the function passed.
// It is meant to be used from background threads to ask something to the main
// thread and wait for its result.
// If await is called from the main thread it just executes the function
// and returns the result.
template<typename T>
T await(std::function<T()>&& func)
{
if (ui::is_ui_thread())
return func();
std::promise<T> promise;
std::future<T> future = promise.get_future();
// Queue the event
os::Event ev;
ev.setType(os::Event::Callback);
ev.setCallback([func, &promise]() {
try {
if constexpr (std::is_void<T>())
func();
else
promise.set_value(func());
}
catch (...) {
promise.set_exception(std::current_exception());
}
});
os::queue_event(ev);
// Wait for the future
return future.get();
}
} // namespace ui
#endif