blob: ca6286326ad8c6e2ef32dfe3302843126b1878d8 [file]
#ifdef WITH_RIVE_SCRIPTING
#include "rive/lua/rive_lua_libs.hpp"
#include "rive/assets/script_asset.hpp"
#include "rive/viewmodel/viewmodel_instance.hpp"
#include "rive/async/work_pool.hpp"
#ifdef RIVE_CANVAS
#include "rive/renderer/render_context.hpp"
#endif
#include "lualib.h"
#include <stdio.h>
#include <unordered_set>
#include <string>
#include <string_view>
#include <queue>
#include <vector>
#include <algorithm>
#include <array>
#include <iterator>
using namespace rive;
int luaopen_rive_base(lua_State* L);
int luaopen_rive_math(lua_State* L);
int luaopen_rive_renderer_library(lua_State* L);
int luaopen_rive_properties(lua_State* L);
int luaopen_rive_artboards(lua_State* L);
int luaopen_rive_data_values(lua_State* L);
int luaopen_rive_data_context(lua_State* L);
int luaopen_rive_input(lua_State* L);
int luaopen_rive_contex(lua_State* L);
int luaopen_rive_audio(lua_State* L);
extern "C" int luaopen_rive_buffer_ext(lua_State* L);
static const luaL_Reg lualibs[] = {
{"", luaopen_base},
{LUA_TABLIBNAME, luaopen_table},
{LUA_MATHLIBNAME, luaopen_math},
{"rive", luaopen_rive_base},
{LUA_OSLIBNAME, luaopen_os},
{LUA_STRLIBNAME, luaopen_string},
{LUA_UTF8LIBNAME, luaopen_utf8},
{LUA_BUFFERLIBNAME, luaopen_buffer},
{LUA_BITLIBNAME, luaopen_bit32},
{"math", luaopen_rive_math},
{"renderer", luaopen_rive_renderer_library},
{"properties", luaopen_rive_properties},
{"artboard", luaopen_rive_artboards},
{"dataValue", luaopen_rive_data_values},
{"input", luaopen_rive_input},
{"context", luaopen_rive_contex},
{"dataContext", luaopen_rive_data_context},
{"audio", luaopen_rive_audio},
{"promise", luaopen_rive_promise},
{NULL, NULL},
};
namespace rive
{
int luaopen_rive(lua_State* L)
{
lua_callbacks(L)->useratom =
[](lua_State*, const char* s, size_t l) -> int16_t {
return rive_lua_findAtom(s, l);
};
const luaL_Reg* lib = lualibs;
for (; lib->func; lib++)
{
lua_pushcfunction(L, lib->func, NULL);
lua_pushstring(L, lib->name);
lua_call(L, 1, 0);
}
// Extend the buffer library with SIMD-accelerated functions
// (readf16, writef16, stridedcopy, convert).
luaopen_rive_buffer_ext(L);
return 0;
}
int rive_luaErrorHandler(lua_State* L)
{
ScriptingContext* context =
static_cast<ScriptingContext*>(lua_getthreaddata(L));
context->printError(L);
// Optionally, you can push a new value onto the stack to be returned by
// lua_pcall For example, push a specific error code or a more detailed
// message
const char* error = lua_tostring(L, -1);
lua_pushstring(L, error);
return 1; // Number of return values
// return 0;
}
int rive_lua_pcall(lua_State* state, int nargs, int nresults)
{
ScriptingContext* context =
static_cast<ScriptingContext*>(lua_getthreaddata(state));
int ret = context->pCall(state, nargs, nresults);
#ifdef RIVE_ORE
rive_lua_closeOrphanRenderPass(state);
rive_lua_closeOrphanCanvasFrames(state);
#endif
return ret;
}
int rive_lua_pcall_with_context(lua_State* state,
ScriptedObject* scriptedObject,
int nargs,
int nresults)
{
ScriptingContext* context =
static_cast<ScriptingContext*>(lua_getthreaddata(state));
ScopedScriptedObjectContext scope(context, scriptedObject);
int ret = context->pCall(state, nargs, nresults);
#ifdef RIVE_ORE
rive_lua_closeOrphanRenderPass(state);
rive_lua_closeOrphanCanvasFrames(state);
#endif
return ret;
}
int rive_lua_pushRef(lua_State* state, int ref)
{
lua_checkstack(state, 1);
return lua_rawgeti(state, luaRegistryIndex, ref);
}
void rive_lua_pop(lua_State* state, int count)
{
lua_settop(state, -count - 1);
}
static void* l_alloc(void* ud, void* ptr, size_t osize, size_t nsize)
{
(void)ud;
(void)osize;
if (nsize == 0)
{
free(ptr);
// delete[] (uint8_t*)ptr;
return NULL;
}
else
{
// auto nptr = new uint8_t[nsize];
// memcpy(nptr, ptr, std::min(nsize, osize));
// delete[] (uint8_t*)ptr;
// return nptr;
return realloc(ptr, nsize);
}
}
static const char* registeredCacheTableKey = "_MODULES";
static int checkRegisteredModules(lua_State* L, const char* path)
{
luaL_findtable(L, LUA_REGISTRYINDEX, registeredCacheTableKey, 1);
lua_getfield(L, -1, path);
if (lua_isnil(L, -1))
{
lua_pop(L, 2);
return 0;
}
lua_remove(L, -2);
return 1;
}
static int lua_requireinternal(lua_State* L, const char* requirerChunkname)
{
// Discard extra arguments, we only use path
lua_settop(L, 1);
const char* path = luaL_checkstring(L, 1);
ScriptingContext* context =
static_cast<ScriptingContext*>(lua_getthreaddata(L));
if (checkRegisteredModules(L, path) == 1)
{
return 1;
}
// Record missing dependency if we're registering a module
if (requirerChunkname != nullptr && context != nullptr)
{
context->recordMissingDependency(requirerChunkname, path);
}
luaL_error(L, "require could not find a script named %s", path);
return 0;
}
static int lua_require(lua_State* L)
{
lua_Debug ar;
int level = 1;
do
{
if (!lua_getinfo(L, level++, "s", &ar))
{
luaL_error(L, "require is not supported in this context");
}
} while (ar.what[0] == 'C');
return lua_requireinternal(L, ar.source);
}
static int luaR_error(lua_State* L)
{
int level = luaL_optinteger(L, 2, 1);
lua_settop(L, 1);
if (lua_isstring(L, 1) && level > 0)
{
luaL_where(L, level);
lua_pushvalue(L, 1);
lua_concat(L, 2);
}
lua_error(L);
}
static int lua_late(lua_State* L)
{
lua_pushnil(L);
return 1;
}
void ScriptingVM::init(lua_State* state, ScriptingContext* context)
{
luaopen_rive(state);
lua_setthreaddata(state, context);
lua_pushcclosurek(state, lua_require, "require", 0, nullptr);
lua_setglobal(state, "require");
lua_pushcclosurek(state, luaR_error, "error", 0, nullptr);
lua_setglobal(state, "error");
lua_pushcclosurek(state, lua_late, "late", 0, nullptr);
lua_setglobal(state, "late");
luaL_sandbox(state);
luaL_sandboxthread(state);
}
ScriptingVM::ScriptingVM(std::unique_ptr<ScriptingContext> context) :
m_ownedContext(std::move(context))
{
m_state = lua_newstate(l_alloc, nullptr);
init(m_state, m_ownedContext.get());
}
ScriptingVM::~ScriptingVM() { closeLuaState(); }
void ScriptingVM::closeLuaState()
{
if (m_state == nullptr)
{
return;
}
// Cancel async tasks before closing Lua state to prevent callbacks
// from accessing dead state.
if (m_ownedContext)
m_ownedContext->shutdownAsyncForState(m_state);
// Null every registered ScriptedObject's back-pointer before the lua
// teardown cascade. Once m_vm is gone, scriptDispose() (called from
// cascading destruction inside lua_close) skips its releaseRef /
// disposeScriptedContext calls. Those calls would otherwise dereference
// Lua userdatas (ScriptedContext, ref tables) that lua_close has already
// freed in earlier sweep iterations.
detachScriptedObjects();
lua_State* state = m_state;
m_state = nullptr;
lua_close(state);
}
void ScriptingVM::replaceContext(std::unique_ptr<ScriptingContext> newContext)
{
#ifdef WITH_RIVE_TOOLS
if (m_ownedContext != nullptr)
{
m_ownedContext->disposeOrphanScriptedProperties();
}
#endif
m_ownedContext = std::move(newContext);
lua_setthreaddata(m_state, m_ownedContext.get());
}
void ScriptingVM::addModule(ModuleDetails* moduleDetails)
{
context()->addModule(moduleDetails);
}
void ScriptingVM::performRegistration()
{
context()->performRegistration(m_state);
}
// Loads bytecode into a sandboxed thread without executing it.
// On success, pushes the module thread (with loaded closure) onto L's stack.
// Returns true on success.
bool ScriptingVM::loadModule(lua_State* L,
const char* name,
Span<uint8_t> bytecode)
{
if (bytecode.empty())
{
return false;
}
// module needs to run in a new thread, isolated from the rest
// note: we create ML on main thread so that it doesn't inherit environment
// of L
lua_State* GL = lua_mainthread(L);
lua_State* ML = lua_newthread(GL);
lua_xmove(GL, L, 1);
// new thread needs to have the globals sandboxed
luaL_sandboxthread(ML);
lua_setthreaddata(ML, lua_getthreaddata(L));
int status =
luau_load(ML, name, (const char*)bytecode.data(), bytecode.size(), 0);
if (status != 0)
{
// luau_load failed — error string is on ML stack
lua_xmove(ML, L, 1);
ScriptingContext* context =
static_cast<ScriptingContext*>(lua_getthreaddata(L));
context->printError(L);
lua_pop(L, 2); // pop error + thread
return false;
}
// Thread with loaded closure is on top of L's stack.
return true;
}
// Executes a previously loaded module thread (on top of L's stack from
// loadModule). On success, replaces the thread with the module result.
// If isUtility, also registers the result in the require cache.
// Returns true on success.
bool ScriptingVM::executeModule(lua_State* L,
const char* name,
bool isUtility,
const char* chunkname)
{
const char* display = chunkname != nullptr ? chunkname : name;
// The module thread should be on top of the stack.
lua_State* ML = lua_tothread(L, -1);
if (ML == nullptr)
{
return false;
}
int status = lua_resume(ML, L, 0);
if (status == 0)
{
if (lua_gettop(ML) == 0)
{
lua_pushfstring(ML, "%s:1: module must return a value", display);
}
else if (!lua_istable(ML, -1) && !lua_isfunction(ML, -1))
{
lua_pushfstring(ML,
"%s:1: module must return a table or function",
display);
}
}
else if (status == LUA_YIELD)
{
lua_pushfstring(ML, "%s:1: module can not yield", display);
}
else if (!lua_isstring(ML, -1))
{
lua_pushfstring(ML,
"%s:1: unknown error while running module",
display);
}
// add ML result to L stack
lua_xmove(ML, L, 1);
// An error occurred if the top of the stack is a string.
if (lua_isstring(L, -1))
{
ScriptingContext* context =
static_cast<ScriptingContext*>(lua_getthreaddata(L));
context->printError(L);
lua_pop(L, 2); // pop error + thread
return false;
}
// remove ML thread from L stack
lua_remove(L, -2);
// added one value to L stack: module result
if (isUtility)
{
// Register into the require cache directly.
luaL_findtable(L, LUA_REGISTRYINDEX, registeredCacheTableKey, 1);
lua_pushstring(L, name);
lua_pushvalue(L, -3); // copy module result (below cache table + name)
lua_settable(L, -3); // cache[name] = result
lua_pop(L, 1); // pop cache table
}
return true;
}
static void dump_stack(lua_State* state)
{
int i;
int top = lua_gettop(state);
for (i = 1; i <= top; i++)
{ /* repeat for each level */
int t = lua_type(state, i);
switch (t)
{
case LUA_TSTRING: /* strings */
fprintf(stderr,
" (%i)[STRING] %s\n",
i,
lua_tostring(state, i));
break;
case LUA_TBOOLEAN: /* booleans */
fprintf(stderr,
" (%i)[BOOLEAN] %s\n",
i,
lua_toboolean(state, i) ? "true" : "false");
break;
case LUA_TNUMBER: /* numbers */
fprintf(stderr,
" (%i)[NUMBER] %g\n",
i,
lua_tonumber(state, i));
break;
default: /* other values */
fprintf(stderr, " (%i)[%s]\n", i, lua_typename(state, t));
break;
}
}
fprintf(stderr, "\n"); /* end the listing */
}
void ScriptingVM::dumpStack(lua_State* state) { dump_stack(state); }
void ScriptingContext::addModule(ModuleDetails* moduleDetails)
{
m_modulesToRegister.push_back(moduleDetails);
m_moduleLookup[moduleDetails->moduleName()] = moduleDetails;
}
bool ScriptingContext::tryRegisterModule(lua_State* state,
ModuleDetails* moduleDetails)
{
#ifndef WITH_RIVE_TOOLS
// In production builds, only allow verified (signed) scripts
if (!moduleDetails->verified())
{
return false;
}
#endif
std::string name = moduleDetails->moduleName();
bool registerSuccess = false;
int functionRef = 0;
if (moduleDetails->isProtocolScript())
{
if (ScriptingVM::registerScript(state,
name.c_str(),
moduleDetails->moduleBytecode()))
{
// registerScript leaves the function on the stack
if (static_cast<lua_Type>(lua_type(state, -1)) == LUA_TFUNCTION)
{
functionRef = lua_ref(state, -1);
}
lua_pop(state, 1);
registerSuccess = true;
}
}
else
{
if (ScriptingVM::registerModule(state,
name.c_str(),
moduleDetails->moduleBytecode()))
{
registerSuccess = true;
}
}
if (registerSuccess)
{
moduleDetails->registrationComplete(functionRef);
onModuleRegistered(moduleDetails);
return true;
}
return false;
}
void ScriptingContext::performRegistration(lua_State* state)
{
// Loop over all of the modules once. We need do a tryRegister
// pass on each module in order to determine if it has any
// required dependencies
for (ModuleDetails* moduleDetails : m_modulesToRegister)
{
if (moduleDetails == nullptr)
{
continue;
}
std::string cacheKey = moduleDetails->moduleName();
// Skip if already registered
if (checkRegisteredModules(state, cacheKey.c_str()) == 1)
{
lua_pop(state, 1);
continue;
}
tryRegisterModule(state, moduleDetails);
}
// One cleanup pass: producers emit modules in dependency order (the
// editor and rive-cli both sort), so a single sorted retry suffices.
if (!m_pendingModules.empty())
{
std::vector<ModuleDetails*> pendingModules;
for (auto module : m_pendingModules)
{
pendingModules.push_back(module);
}
std::vector<ModuleDetails*> sortedModules;
std::unordered_set<ModuleDetails*> visitedModules;
ModuleDetails* module = pendingModules.back();
pendingModules.pop_back();
sortNextModule(module,
&pendingModules,
&sortedModules,
&visitedModules);
// Register modules in sorted order
for (ModuleDetails* moduleDetails : sortedModules)
{
// Skip utility modules that already made it into the cache.
if (checkRegisteredModules(state,
moduleDetails->moduleName().c_str()) ==
1)
{
lua_pop(state, 1);
continue;
}
// A known-missing require has not registered yet; re-running
// the chunk would repeat its side effects and error.
if (!moduleDetails->missingDependencies().empty())
{
continue;
}
tryRegisterModule(state, moduleDetails);
}
}
m_modulesToRegister.clear();
m_pendingModules.clear();
}
void ScriptingContext::sortNextModule(
ModuleDetails* module,
std::vector<ModuleDetails*>* pendingModules,
std::vector<ModuleDetails*>* sortedModules,
std::unordered_set<ModuleDetails*>* visitedModules)
{
// If already visited, skip
if (visitedModules->find(module) != visitedModules->end())
{
return;
}
auto dependencies = module->missingDependencies();
for (const auto& dependencyName : dependencies)
{
auto lookupIt = m_moduleLookup.find(dependencyName);
if (lookupIt != m_moduleLookup.end())
{
ModuleDetails* dependencyModule = lookupIt->second;
// Recursively process the dependency
sortNextModule(dependencyModule,
pendingModules,
sortedModules,
visitedModules);
}
}
if (std::find(sortedModules->begin(), sortedModules->end(), module) ==
sortedModules->end())
{
sortedModules->push_back(module);
}
visitedModules->insert(module);
if (!pendingModules->empty())
{
ModuleDetails* nextModule = pendingModules->back();
pendingModules->pop_back();
sortNextModule(nextModule,
pendingModules,
sortedModules,
visitedModules);
}
}
void ScriptingContext::recordMissingDependency(
const std::string& requiringModule,
const std::string& missingModule)
{
if (requiringModule.empty())
{
return;
}
auto it = m_moduleLookup.find(requiringModule);
if (it == m_moduleLookup.end())
{
return;
}
it->second->addMissingDependency(missingModule);
m_pendingModules.insert(it->second);
}
void ScriptingContext::onModuleRegistered(ModuleDetails* moduleDetails)
{
std::string key = moduleDetails->moduleName();
for (ModuleDetails* module : m_modulesToRegister)
{
if (!module->missingDependencies().empty())
{
module->clearMissingDependency(key);
}
}
auto it = m_pendingModules.find(moduleDetails);
if (it != m_pendingModules.end())
{
m_pendingModules.erase(it);
}
}
#ifdef WITH_RIVE_TOOLS
void ScriptingContext::registerShaderRstb(std::string name,
std::vector<uint8_t> bytes)
{
m_shaderRstbs[std::move(name)] = std::move(bytes);
}
const std::vector<uint8_t>* ScriptingContext::findShaderRstb(
const std::string& name) const
{
auto it = m_shaderRstbs.find(name);
return it != m_shaderRstbs.end() ? &it->second : nullptr;
}
const std::vector<uint8_t>* ScriptingContext::findShaderRstb(
const ScopedAssetReference& reference) const
{
const std::vector<uint8_t>* found = nullptr;
int bestRank = 0;
for (const auto& entry : m_shaderRstbs)
{
size_t slash = entry.first.rfind('/');
std::string shortName = slash == std::string::npos
? entry.first
: entry.first.substr(slash + 1);
int rank = reference.match(entry.first, shortName);
if (rank > bestRank)
{
bestRank = rank;
found = &entry.second;
}
}
return found;
}
void ScriptingContext::setGeneratorRef(uint32_t assetId, int ref)
{
m_assetGeneratorRefs[assetId] = ref;
}
int ScriptingContext::getGeneratorRef(uint32_t assetId) const
{
auto it = m_assetGeneratorRefs.find(assetId);
return it != m_assetGeneratorRefs.end() ? it->second : 0;
}
void ScriptingContext::clearGeneratorRefs() { m_assetGeneratorRefs.clear(); }
bool ScriptingContext::hasGeneratorRef(uint32_t assetId) const
{
return m_assetGeneratorRefs.find(assetId) != m_assetGeneratorRefs.end();
}
void ScriptingContext::trackOrphanScriptedProperty(ScriptedProperty* property)
{
if (property != nullptr)
{
m_orphanScriptedProperties.push_back(property);
}
}
void ScriptingContext::untrackOrphanScriptedProperty(ScriptedProperty* property)
{
auto it = std::remove(m_orphanScriptedProperties.begin(),
m_orphanScriptedProperties.end(),
property);
m_orphanScriptedProperties.erase(it, m_orphanScriptedProperties.end());
}
void ScriptingContext::disposeOrphanScriptedProperties()
{
auto orphans = m_orphanScriptedProperties;
for (ScriptedProperty* property : orphans)
{
if (property != nullptr)
{
property->dispose();
}
}
m_orphanScriptedProperties.clear();
}
void ScriptingContext::disposeOrphanScriptedProperties(uint32_t tag)
{
if (tag == 0)
{
return;
}
// dispose() untracks each property, so iterate a copy.
auto orphans = m_orphanScriptedProperties;
for (ScriptedProperty* property : orphans)
{
if (property != nullptr && property->orphanOwnerTag() == tag)
{
property->dispose();
}
}
}
#endif
void ScriptingContext::trackViewModelInstance(rcp<ViewModelInstance> instance)
{
if (instance == nullptr)
{
return;
}
auto& tracked = m_trackedViewModelInstances[instance.get()];
tracked.instance = instance;
tracked.registrations++;
}
void ScriptingContext::untrackViewModelInstance(ViewModelInstance* instance)
{
if (instance == nullptr)
{
return;
}
auto it = m_trackedViewModelInstances.find(instance);
if (it != m_trackedViewModelInstances.end() &&
--it->second.registrations <= 0)
{
m_trackedViewModelInstances.erase(it);
}
}
void ScriptingContext::advanceDetachedViewModels()
{
for (auto& entry : m_trackedViewModelInstances)
{
ViewModelInstance* instance = entry.second.instance.get();
// Only advance detached roots. Instances with parents are already
// reached through the bound tree or their detached-root ancestor.
if (!instance->hasParents())
{
instance->advanced();
}
}
}
// ── WorkPool integration ───────────────────────────────────────────────────
// getGlobalWorkPool() is defined in work_pool.cpp (shared singleton).
WorkPool* ScriptingContext::workPool()
{
if (m_ownerId == 0)
m_ownerId = WorkPool::nextOwnerId();
return getGlobalWorkPool().get();
}
// Forward-declared in lua_image_decode.cpp (WASM only).
#ifdef __EMSCRIPTEN__
extern void wasm_cancelPendingDecodes(lua_State* mainThread);
#endif
void ScriptingContext::shutdownAsync()
{
if (m_ownerId != 0)
{
auto& pool = getGlobalWorkPoolIfExists();
if (pool)
pool->cancelAllForOwner(m_ownerId);
m_ownerId = 0;
}
}
// Called from ~ScriptingVM before lua_close. On WASM, also cancel
// browser-native image decodes that bypass WorkPool.
void ScriptingContext::shutdownAsyncForState(lua_State* mainThread)
{
shutdownAsync();
#ifdef __EMSCRIPTEN__
wasm_cancelPendingDecodes(mainThread);
#endif
}
bool ScriptingVM::registerScript(lua_State* state,
const char* name,
Span<uint8_t> bytecode,
const char* chunkname)
{
// Check if already registered - leave module on stack for caller to use
if (checkRegisteredModules(state, name) == 1)
{
return true;
}
if (!loadModule(state, chunkname != nullptr ? chunkname : name, bytecode))
{
return false;
}
if (!executeModule(state, name, false, chunkname))
{
return false;
}
return true;
}
bool ScriptingVM::registerModule(lua_State* state,
const char* name,
Span<uint8_t> bytecode,
const char* chunkname)
{
// Check if already registered
if (checkRegisteredModules(state, name) == 1)
{
lua_pop(state, 1);
return true;
}
if (!loadModule(state, chunkname != nullptr ? chunkname : name, bytecode))
{
return false;
}
if (!executeModule(state, name, true, chunkname))
{
return false;
}
// executeModule with isUtility=true registers into the require cache
// and leaves the module result on the stack. Pop it.
lua_pop(state, 1);
return true;
}
void ScriptingVM::unregisterModule(lua_State* state, const char* name)
{
luaL_findtable(state, LUA_REGISTRYINDEX, registeredCacheTableKey, 1);
lua_pushstring(state, name);
lua_pushnil(state);
lua_settable(state, -3);
lua_pop(state, 1);
}
void ScriptingVM::unregisterModule(const char* name)
{
return unregisterModule(m_state, name);
}
bool ScriptingVM::registerModule(const char* name, Span<uint8_t> bytecode)
{
return registerModule(m_state, name, bytecode);
}
bool ScriptingVM::registerScript(const char* name, Span<uint8_t> bytecode)
{
return registerScript(m_state, name, bytecode);
}
int CPPRuntimeScriptingContext::pCall(lua_State* state, int nargs, int nresults)
{
// calculate stack position for message handler
int hpos = lua_gettop(state) - nargs;
lua_pushcfunction(state, rive_luaErrorHandler, "riveErrorHandler");
lua_insert(state, hpos);
startTimedExecution(state);
int ret = lua_pcall(state, nargs, nresults, hpos);
endTimedExecution(state);
lua_remove(state, hpos);
return ret;
}
} // namespace rive
#endif