Add a class to register stateful callbacks to JS

Use the class in autostart as an example.
This commit is contained in:
phosit 2025-02-17 20:01:04 +01:00 committed by phosit
parent 0afb5f3d06
commit 44605c1297
3 changed files with 157 additions and 33 deletions

View file

@ -736,34 +736,6 @@ CStr8 LoadSettingsOfScenarioMap(const VfsPath &mapPath)
return mapElement.GetText();
}
// TODO: this essentially duplicates the CGUI logic to load directory or scripts.
// NB: this won't make sure to not double-load scripts, unlike the GUI.
void AutostartLoadScript(const ScriptInterface& scriptInterface, const VfsPath& path)
{
if (path.IsDirectory())
{
VfsPaths pathnames;
vfs::GetPathnames(g_VFS, path, L"*.js", pathnames);
for (const VfsPath& file : pathnames)
scriptInterface.LoadGlobalScriptFile(file);
}
else
scriptInterface.LoadGlobalScriptFile(path);
}
// TODO: this essentially duplicates the CGUI function
CParamNode GetTemplate(const std::string& templateName)
{
// This is very cheap to create so let's just do it every time.
CTemplateLoader templateLoader;
const CParamNode& templateRoot = templateLoader.GetTemplateFileData(templateName).GetOnlyChild();
if (!templateRoot.IsOk())
LOGERROR("Invalid template found for '%s'", templateName.c_str());
return templateRoot;
}
/**
* Autostart arguments are parsed in javascript for convenience and moddability.
* This C++ part only handles the following arguments:
@ -785,13 +757,50 @@ bool Autostart(const CmdLineArgs& args)
// We use the javascript gameSettings to handle options, but that requires running JS.
// Since we don't want to use the full Gui manager, we load an entrypoint script
// that can run the priviledged "LoadScript" function, and then call the appropriate function.
ScriptFunction::Register<&AutostartLoadScript>(rq, "LoadScript");
// TODO: this essentially duplicates the CGUI logic to load directory or scripts.
std::unordered_set<VfsPath> templateCache;
const auto autostartLoadScript = [&templateCache](const ScriptInterface& scriptInterface,
const VfsPath& path)
{
if (!std::get<1>(templateCache.insert(path)))
return;
if (path.IsDirectory())
{
VfsPaths pathnames;
vfs::GetPathnames(g_VFS, path, L"*.js", pathnames);
for (const VfsPath& file : pathnames)
scriptInterface.LoadGlobalScriptFile(file);
}
else
scriptInterface.LoadGlobalScriptFile(path);
};
const auto loadScriptCallback = ScriptFunction::Register(rq, "LoadScript", autostartLoadScript);
// Load the entire folder to allow mods to extend the entrypoint without copying the whole file.
AutostartLoadScript(scriptInterface, VfsPath(L"autostart/"));
autostartLoadScript(scriptInterface, VfsPath(L"autostart/"));
// Provide some required functions to the script.
struct GetTemplate
{
CTemplateLoader templateLoader;
CParamNode operator()(const std::string& templateName){
// TODO: this essentially duplicates the CGUI function
const CParamNode& templateRoot{
templateLoader.GetTemplateFileData(templateName).GetOnlyChild()};
if (!templateRoot.IsOk())
LOGERROR("Invalid template found for '%s'", templateName.c_str());
return templateRoot;
}
};
std::optional<ScriptFunction::StatefulCallback<GetTemplate>> getTemplateCallback;
if (args.Has("autostart-nonvisual"))
ScriptFunction::Register<&GetTemplate>(rq, "GetTemplate");
getTemplateCallback.emplace(rq, "GetTemplate", GetTemplate{});
else
{
JSI_GUIManager::RegisterScriptFunctions(rq);

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2024 Wildfire Games.
/* Copyright (C) 2025 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -23,9 +23,11 @@
#include "ScriptRequest.h"
#include <fmt/format.h>
#include <memory>
#include <tuple>
#include <type_traits>
#include <stdexcept>
#include <string>
#include <utility>
class ScriptInterface;
@ -257,6 +259,13 @@ private:
return !ScriptException::CatchPending(rq) && success;
}
struct StatefullCallbackPrivateSlot
{
static constexpr size_t callback{0};
static constexpr size_t classInfo{1};
static constexpr uint32_t count{2};
};
///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
public:
@ -453,6 +462,92 @@ public:
{
JS_DefineFunction(cx, scope, name, &ToJSNative<callable, thisGetter>, args_info<decltype(callable)>::nb_args, flags);
}
template<typename Callable>
class StatefulCallback
{
class ClassInfo
{
public:
ClassInfo(std::string className) :
name{std::move(className)}
{}
JSClassOps classOps{nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
/*.finalize = */finalize, /*.call = */&ToJSNative<&Callable::operator(), getter>,
nullptr, nullptr};
std::string name;
JSClass jsClass{name.c_str(),
JSCLASS_HAS_RESERVED_SLOTS(StatefullCallbackPrivateSlot::count) |
JSCLASS_BACKGROUND_FINALIZE,
&classOps};
};
public:
explicit StatefulCallback(const ScriptRequest& rq, std::string name, Callable callable) :
StatefulCallback{rq, std::move(name),
JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT, std::move(callable)}
{}
explicit StatefulCallback(const ScriptRequest& rq, std::string name, const unsigned flags,
Callable callable) :
callback{std::move(callable)},
functionObject{rq.cx}
{
auto classInfo = std::make_unique<ClassInfo>(std::move(name));
functionObject.set(JS_NewObject(rq.cx, &classInfo->jsClass));
JS::RootedValue functionValue{rq.cx, JS::ObjectValue(*functionObject)};
if (!JS_DefineProperty(rq.cx, rq.nativeScope, classInfo->name.c_str(), functionValue,
flags))
{
throw std::runtime_error{fmt::format(
"Failed defining function {:?} on the native scope.", classInfo->name)};
}
JS::SetReservedSlot(functionObject, StatefullCallbackPrivateSlot::classInfo,
JS::PrivateValue(classInfo.release()));
JS::SetReservedSlot(functionObject, StatefullCallbackPrivateSlot::callback,
JS::PrivateValue(&callback));
}
StatefulCallback(const StatefulCallback&) = delete;
StatefulCallback& operator=(const StatefulCallback&) = delete;
StatefulCallback(StatefulCallback&&) = delete;
StatefulCallback& operator=(StatefulCallback&&) = delete;
~StatefulCallback()
{
JS::SetReservedSlot(functionObject, StatefullCallbackPrivateSlot::callback,
JS::UndefinedValue());
}
private:
static Callable* getter(const ScriptRequest&, JS::CallArgs& args)
{
return JS::GetMaybePtrFromReservedSlot<Callable>(&args.callee(),
StatefullCallbackPrivateSlot::callback);
}
static void finalize(JS::GCContext*, JSObject* obj)
{
delete JS::GetMaybePtrFromReservedSlot<ClassInfo>(obj,
StatefullCallbackPrivateSlot::classInfo);
}
Callable callback;
JS::RootedObject functionObject;
};
template<typename Callable>
static StatefulCallback<Callable> Register(const ScriptRequest& rq, std::string name,
const unsigned flags, Callable callable)
{
return StatefulCallback{rq, std::move(name), flags, std::move(callable)};
}
template<typename Callable>
static StatefulCallback<Callable> Register(const ScriptRequest& rq, std::string name,
Callable callable)
{
return StatefulCallback{rq, std::move(name), std::move(callable)};
}
};
#endif // INCLUDED_FUNCTIONWRAPPER

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2021 Wildfire Games.
/* Copyright (C) 2025 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -110,4 +110,24 @@ public:
TS_ASSERT_EQUALS(ret, 4);
}
}
void test_statefull()
{
ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script};
JS::RootedValue nativeScope{rq.cx, JS::ObjectValue(*rq.nativeScope)};
constexpr const char* name{"callback"};
{
bool called{false};
auto _ = ScriptFunction::Register(rq, name, [&](){
called = true;
});
TS_ASSERT(!called);
TS_ASSERT(ScriptFunction::CallVoid(rq, nativeScope, name));
TS_ASSERT(called);
}
TS_ASSERT(!ScriptFunction::CallVoid(rq, nativeScope, name));
}
};