[lua] Add require() function (fix aseprite/api#10)

This is the first attempt to finally implement the require() function
on Lua. The main problem was how to solve conflicts between plugins
that use the same library name. Here we separate each plugin like in a
namespace, so require(name) inside a plugin will save the module in
_LOADED["pluginName/libraryName"] to avoid conflicts with other
libraryName from other plugins.
This commit is contained in:
David Capello 2023-04-18 19:41:01 -03:00
parent 8ff62f7a5f
commit 1c6e583c87
13 changed files with 212 additions and 18 deletions

View File

@ -196,6 +196,7 @@ if(ENABLE_SCRIPTING)
script/properties_class.cpp
script/range_class.cpp
script/rectangle_class.cpp
script/require.cpp
script/security.cpp
script/selection_class.cpp
script/site_class.cpp

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2018-2023 Igara Studio S.A.
// Copyright (C) 2016-2018 David Capello
//
// This program is distributed under the terms of
@ -137,7 +137,7 @@ int DefaultCliDelegate::execScript(const std::string& filename,
const Params& params)
{
auto engine = App::instance()->scriptEngine();
if (!engine->evalFile(filename, params))
if (!engine->evalUserFile(filename, params))
throw base::Exception("Error executing script %s", filename.c_str());
return engine->returnCode();
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2018-2023 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -78,7 +78,7 @@ void RunScriptCommand::onExecute(Context* context)
App::instance()
->scriptEngine()
->evalFile(m_filename, m_params);
->evalUserFile(m_filename, m_params);
#if ENABLE_UI
if (context->isUIAvailable())

View File

@ -35,6 +35,7 @@
#ifdef ENABLE_SCRIPTING
#include "app/script/engine.h"
#include "app/script/luacpp.h"
#include "app/script/require.h"
#endif
#include "archive.h"
@ -627,6 +628,10 @@ void Extension::initScripts()
script::push_plugin(L, this);
m_plugin.pluginRef = luaL_ref(L, LUA_REGISTRYINDEX);
// Set the _PLUGIN global so require() can find .lua files from the
// plugin path.
script::SetPluginForRequire setPlugin(L, m_plugin.pluginRef);
// Read plugin.preferences value
{
std::string fn = base::join_path(m_path, kPrefLua);
@ -665,7 +670,7 @@ void Extension::initScripts()
lua_pop(L, 1);
}
// Call the init() function of thi sscript with a Plugin object as first parameter
// Call the init() function of this script with a Plugin object as first parameter
if (lua_getglobal(L, "init") == LUA_TFUNCTION) {
// Call init(plugin)
lua_rawgeti(L, LUA_REGISTRYINDEX, m_plugin.pluginRef);

View File

@ -18,6 +18,7 @@
#include "app/pref/preferences.h"
#include "app/script/blend_mode.h"
#include "app/script/luacpp.h"
#include "app/script/require.h"
#include "app/script/security.h"
#include "app/sprite_sheet_type.h"
#include "app/tilemap_mode.h"
@ -217,16 +218,7 @@ Engine::Engine()
#endif
// Standard Lua libraries
luaL_requiref(L, LUA_GNAME, luaopen_base, 1);
luaL_requiref(L, LUA_COLIBNAME, luaopen_coroutine, 1);
luaL_requiref(L, LUA_TABLIBNAME, luaopen_table, 1);
luaL_requiref(L, LUA_IOLIBNAME, luaopen_io, 1);
luaL_requiref(L, LUA_OSLIBNAME, luaopen_os, 1);
luaL_requiref(L, LUA_STRLIBNAME, luaopen_string, 1);
luaL_requiref(L, LUA_MATHLIBNAME, luaopen_math, 1);
luaL_requiref(L, LUA_UTF8LIBNAME, luaopen_utf8, 1);
luaL_requiref(L, LUA_DBLIBNAME, luaopen_debug, 1);
lua_pop(L, 9);
luaL_openlibs(L);
// Overwrite Lua functions
lua_register(L, "print", print);
@ -255,6 +247,9 @@ Engine::Engine()
lua_setfield(L, -2, "execute");
lua_pop(L, 1);
// Enhance require() function for plugins
custom_require_function(L);
// Generic code used by metatables
run_mt_index_code(L);
@ -579,7 +574,7 @@ bool Engine::evalFile(const std::string& filename,
}
std::string absFilename = base::get_absolute_path(filename);
AddScriptFilename add(absFilename);
AddScriptFilename addScript(absFilename);
set_app_params(L, params);
if (g_debuggerDelegate)
@ -593,6 +588,18 @@ bool Engine::evalFile(const std::string& filename,
return result;
}
bool Engine::evalUserFile(const std::string& filename,
const Params& params)
{
// Set the _SCRIPT_PATH global so require() can find .lua files from
// the script path.
std::string path =
base::get_file_path(
base::get_absolute_path(filename));
SetScriptForRequire setScript(L, path.c_str());
return evalFile(filename, params);
}
void Engine::startDebugger(DebuggerDelegate* debuggerDelegate)
{
g_debuggerDelegate = debuggerDelegate;

View File

@ -96,6 +96,8 @@ namespace app {
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());
void consolePrint(const char* text) {
onConsolePrint(text);

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018 Igara Studio S.A.
// Copyright (C) 2018-2023 Igara Studio S.A.
// Copyright (C) 2018 David Capello
//
// This program is distributed under the terms of
@ -44,6 +44,8 @@ void run_mt_index_code(lua_State* L)
const char* s = lua_tostring(L, -1);
if (s)
std::puts(s);
lua_pop(L, 1);
}
}

View File

@ -313,6 +313,20 @@ int Plugin_newMenuSeparator(lua_State* L)
return 0;
}
int Plugin_get_name(lua_State* L)
{
auto plugin = get_obj<Plugin>(L, 1);
lua_pushstring(L, plugin->ext->name().c_str());
return 1;
}
int Plugin_get_path(lua_State* L)
{
auto plugin = get_obj<Plugin>(L, 1);
lua_pushstring(L, plugin->ext->path().c_str());
return 1;
}
int Plugin_get_preferences(lua_State* L)
{
if (!lua_getuservalue(L, 1)) {
@ -341,6 +355,8 @@ const luaL_Reg Plugin_methods[] = {
};
const Property Plugin_properties[] = {
{ "name", Plugin_get_name, nullptr },
{ "path", Plugin_get_path, nullptr },
{ "preferences", Plugin_get_preferences, Plugin_set_preferences },
{ nullptr, nullptr, nullptr }
};

105
src/app/script/require.cpp Normal file
View File

@ -0,0 +1,105 @@
// Aseprite
// Copyright (c) 2023 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/script/require.h"
#include "app/extensions.h"
#include <cstring>
namespace app {
namespace script {
static void eval_code(lua_State* L, const char* code)
{
if (luaL_loadbuffer(L, code, std::strlen(code), "internal") ||
lua_pcall(L, 0, 0, 0)) {
// Error case
const char* s = lua_tostring(L, -1);
if (s)
std::puts(s);
lua_pop(L, 1);
}
}
void custom_require_function(lua_State* L)
{
eval_code(L, R"(
_PACKAGE_PATH_STACK = {}
local origRequire = require
function require(name)
if _PLUGIN then
local module = origRequire(_PLUGIN.name .. '/' .. name)
if module then return module end
end
return origRequire(name)
end
local origLuaSearcher = package.searchers[2]
package.searchers[2] = function(name)
if _PLUGIN then
name = name:sub(#_PLUGIN.name+2)
end
return origLuaSearcher(name)
end
)");
}
SetPluginForRequire::SetPluginForRequire(lua_State* L, int pluginRef) : L(L)
{
lua_rawgeti(L, LUA_REGISTRYINDEX, pluginRef);
lua_setglobal(L, "_PLUGIN");
// We could use all elements of package.config, but "/?.lua;" should work:
//
// local templateSep = package.config:sub(3, 3)
// local substitutionPoint = package.config:sub(5, 5)
//
eval_code(L, R"(
table.insert(_PACKAGE_PATH_STACK, package.path)
local pathSep = package.config:sub(1, 1)
package.path = _PLUGIN.path .. pathSep .. '?.lua;' .. package.path
)");
}
SetPluginForRequire::~SetPluginForRequire()
{
eval_code(L, R"(
package.path = table.remove(_PACKAGE_PATH_STACK)
_PLUGIN_OLDPATH = nil
_PLUGIN = nil
)");
}
SetScriptForRequire::SetScriptForRequire(lua_State* L, const char* path) : L(L)
{
lua_pushstring(L, path);
lua_setglobal(L, "_SCRIPT_PATH");
eval_code(L, R"(
table.insert(_PACKAGE_PATH_STACK, package.path)
local pathSep = package.config:sub(1, 1)
package.path = _SCRIPT_PATH .. pathSep .. '?.lua;' .. package.path
)");
}
SetScriptForRequire::~SetScriptForRequire()
{
eval_code(L, R"(
package.path = table.remove(_PACKAGE_PATH_STACK)
_SCRIPT_PATH = nil
)");
}
} // namespace script
} // namespace app

39
src/app/script/require.h Normal file
View File

@ -0,0 +1,39 @@
// Aseprite
// Copyright (c) 2023 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_SCRIPT_REQUIRE_H_INCLUDED
#define APP_SCRIPT_REQUIRE_H_INCLUDED
#pragma once
#include "app/script/luacpp.h"
#include <string>
namespace app {
namespace script {
class SetPluginForRequire {
public:
SetPluginForRequire(lua_State* L, int pluginRef);
~SetPluginForRequire();
private:
lua_State* L;
};
class SetScriptForRequire {
public:
SetScriptForRequire(lua_State* L, const char* path);
~SetScriptForRequire();
private:
lua_State* L;
};
void custom_require_function(lua_State* L);
} // namespace script
} // namespace app
#endif

11
tests/scripts/require.lua Normal file
View File

@ -0,0 +1,11 @@
-- Copyright (c) 2023 Igara Studio S.A.
--
-- This file is released under the terms of the MIT license.
-- Read LICENSE.txt for more information.
local mod = require 'require_module'
assert(mod.a == 5)
mod.a = 6
local mod2 = require 'require_module'
assert(mod2.a == 6) -- Now mod and mod2 points to the same object

View File

@ -0,0 +1,6 @@
-- Copyright (c) 2023 Igara Studio S.A.
--
-- This file is released under the terms of the MIT license.
-- Read LICENSE.txt for more information.
return { a=5 }

2
third_party/lua vendored

@ -1 +1 @@
Subproject commit db8284f5fc6f02f8f800c99b84b5e4f640e893ce
Subproject commit 04abf20c9e8c973c62b30d0b98f6884ff665b1ba