mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
364 lines
16 KiB
C++
364 lines
16 KiB
C++
//
|
|
// ██████ ██ ██ ██████ ██████
|
|
// ██ ██ ██ ██ ██ ██ ** Classy Header-Only Classes **
|
|
// ██ ███████ ██ ██ ██
|
|
// ██ ██ ██ ██ ██ ██ https://github.com/Tracktion/choc
|
|
// ██████ ██ ██ ██████ ██████
|
|
//
|
|
// CHOC is (C)2022 Tracktion Corporation, and is offered under the terms of the ISC license:
|
|
//
|
|
// Permission to use, copy, modify, and/or distribute this software for any purpose with or
|
|
// without fee is hereby granted, provided that the above copyright notice and this permission
|
|
// notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
|
// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
|
|
// CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
|
// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
|
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
|
#ifndef CHOC_JAVASCRIPT_HEADER_INCLUDED
|
|
#define CHOC_JAVASCRIPT_HEADER_INCLUDED
|
|
|
|
#include <stdexcept>
|
|
#include <functional>
|
|
#include <optional>
|
|
#include "../containers/choc_Value.h"
|
|
#include "../text/choc_JSON.h"
|
|
|
|
/**
|
|
Wrapper classes for encapsulating different javascript engines such as
|
|
Duktape and QuickJS.
|
|
|
|
Just use one of the context-creation functions such as
|
|
choc::javascript::createQuickJSContext() to create a context for running
|
|
javascript code.
|
|
*/
|
|
namespace choc::javascript
|
|
{
|
|
/// This is thrown by any javascript functions that need to report an error
|
|
struct Error : public std::runtime_error
|
|
{
|
|
Error (const std::string& error) : std::runtime_error (error) {}
|
|
};
|
|
|
|
//==============================================================================
|
|
/// Helper class to hold and provide access to the arguments in a javascript
|
|
/// function callback.
|
|
struct ArgumentList
|
|
{
|
|
const choc::value::Value* args = nullptr;
|
|
size_t numArgs = 0;
|
|
|
|
/// Returns the number of arguments
|
|
size_t size() const noexcept { return numArgs; }
|
|
/// Returns true if there are no arguments
|
|
bool empty() const noexcept { return numArgs == 0; }
|
|
|
|
/// Returns an argument, or a nullptr if the index is out of range.
|
|
const choc::value::Value* operator[] (size_t index) const { return index < numArgs ? args + index : nullptr; }
|
|
|
|
/// Gets an argument as a primitive type (or a string).
|
|
/// If the index is out of range or the object isn't a suitable type,
|
|
/// then the default value provided will be returned instead.
|
|
template <typename PrimitiveType>
|
|
PrimitiveType get (size_t argIndex, PrimitiveType defaultValue = {}) const;
|
|
|
|
/// Standard iterator
|
|
const choc::value::Value* begin() const noexcept { return args; }
|
|
const choc::value::Value* end() const noexcept { return args + numArgs; }
|
|
};
|
|
|
|
//==============================================================================
|
|
/**
|
|
An execution context which you use for running javascript code.
|
|
|
|
These are really simple to use: call one of the creation functions such
|
|
as choc::javascript::createQuickJSContext() which will give you a shared_ptr to
|
|
a context. Then you can add any native bindings that you need with
|
|
registerFunction(), and call evaluate() or invoke() to execute code or call
|
|
functions directly.
|
|
|
|
These contexts are not thread-safe, so it's up to the caller to handle thread
|
|
synchronisation if using a single context from multiple threads.
|
|
|
|
They're also definitely not realtime-safe: any of the methods may allocate,
|
|
block, or make system calls.
|
|
*/
|
|
class Context
|
|
{
|
|
public:
|
|
/// To create a Context, use a function such as choc::javascript::createQuickJSContext();
|
|
Context() = default;
|
|
Context (Context&&);
|
|
Context& operator= (Context&&);
|
|
~Context();
|
|
|
|
/// Returns true if the context is valid.
|
|
operator bool() const { return pimpl != nullptr; }
|
|
|
|
//==============================================================================
|
|
/// This callback is used by the run() method.
|
|
using CompletionHandler = std::function<void(const std::string& error,
|
|
const choc::value::ValueView& result)>;
|
|
|
|
/// Executes some javascript asynchronously.
|
|
/// If a CompletionHandler callback is provided, it will be called asynchronously
|
|
/// with the return value and any errors that occurred. Note that if you want to execute
|
|
/// the script as a module, use runModule() instead.
|
|
void run (const std::string& javascriptCode,
|
|
CompletionHandler handleResult = {});
|
|
|
|
/// When parsing modules, this function is expected to take a path to a module, and
|
|
/// to return the content of that module, or an empty optional if not found.
|
|
using ReadModuleContentFn = std::function<std::optional<std::string>(std::string_view)>;
|
|
|
|
/// This callback will asynchronously parse the script as a module, and will use the
|
|
/// ReadModuleContentFn functor to resolve any imported modules that it needs. If a
|
|
/// CompletionHandler callback is provided, it will be called asynchronously with the
|
|
/// return value and any errors that occurred.
|
|
/// NB: Not all engines support modules.
|
|
void runModule (const std::string& moduleCode,
|
|
ReadModuleContentFn,
|
|
CompletionHandler handleResult = {});
|
|
|
|
/// Evaluates a javascript expression synchronously, and returns the result.
|
|
/// If there are any parse errors, this will throw a choc::javascript::Error exception.
|
|
/// Note that if you want to execute the script as a module, use runModule() instead.
|
|
choc::value::Value evaluateExpression (const std::string& javascriptCode);
|
|
|
|
/// Attempts to synchronously invoke a global function with no arguments.
|
|
/// Any errors will throw a choc::javascript::Error exception.
|
|
/// None of the methods in this class are either thread-safe or realtime-safe, so you'll
|
|
/// need to organise your own locking if you're calling into a single Context from
|
|
/// multiple threads.
|
|
choc::value::Value invoke (std::string_view functionName);
|
|
|
|
/// Attempts to invoke a global function with the arguments provided.
|
|
/// The arguments can be primitives, strings, choc::value::ValueView or
|
|
/// choc::value::Value types.
|
|
/// Any errors will throw a choc::javascript::Error exception.
|
|
/// None of the methods in this class are either thread-safe or realtime-safe, so you'll
|
|
/// need to organise your own locking if you're calling into a single Context from
|
|
/// multiple threads.
|
|
template <typename... Args>
|
|
choc::value::Value invoke (std::string_view functionName, Args&&... args);
|
|
|
|
/// Attempts to invoke a global function with an array of arguments.
|
|
/// The objects in the argument list can be primitives, strings, choc::value::ValueView
|
|
/// or choc::value::Value types.
|
|
/// Any errors will throw a choc::javascript::Error exception.
|
|
/// None of the methods in this class are either thread-safe or realtime-safe, so you'll
|
|
/// need to organise your own locking if you're calling into a single Context from
|
|
/// multiple threads.
|
|
template <typename ArgList>
|
|
choc::value::Value invokeWithArgList (std::string_view functionName, const ArgList& args);
|
|
|
|
/// This is the prototype for a lambda which can be bound as a javascript function.
|
|
using NativeFunction = std::function<choc::value::Value(ArgumentList)>;
|
|
|
|
/// Binds a lambda function to a global name so that javascript code can invoke it.
|
|
void registerFunction (const std::string& name, NativeFunction fn);
|
|
|
|
/// Pumps the message loop in an engine-specific way - may have no effect on some platforms.
|
|
void pumpMessageLoop();
|
|
|
|
//==============================================================================
|
|
/// @internal
|
|
struct Pimpl;
|
|
/// @internal
|
|
Context (std::unique_ptr<Pimpl>);
|
|
|
|
private:
|
|
std::unique_ptr<Pimpl> pimpl;
|
|
};
|
|
|
|
//==============================================================================
|
|
/// Creates a QuickJS-based context. If you call this, then you'll need to
|
|
/// include choc_javascript_QuickJS.h in one (and only one!) of your source files.
|
|
Context createQuickJSContext();
|
|
|
|
/// Creates a Duktape-based context. If you call this, then you'll need to
|
|
/// include choc_javascript_Duktape.h in one (and only one!) of your source files.
|
|
Context createDuktapeContext();
|
|
|
|
/// Creates a V8-based context. If you call this, then you'll need to
|
|
/// make sure that your project also has the V8 header folder in its
|
|
/// search path, and that you statically link the appropriate V8 libs.
|
|
Context createV8Context();
|
|
|
|
//==============================================================================
|
|
/// Sanitises a string to provide a version of it that is safe for use as a
|
|
/// javascript identifier. This involves removing/replacing any illegal
|
|
/// characters and modifying the string to avoid clashes with reserved words.
|
|
std::string makeSafeIdentifier (std::string name);
|
|
}
|
|
|
|
|
|
//==============================================================================
|
|
// _ _ _ _
|
|
// __| | ___ | |_ __ _ (_)| | ___
|
|
// / _` | / _ \| __| / _` || || |/ __|
|
|
// | (_| || __/| |_ | (_| || || |\__ \ _ _ _
|
|
// \__,_| \___| \__| \__,_||_||_||___/(_)(_)(_)
|
|
//
|
|
// Code beyond this point is implementation detail...
|
|
//
|
|
//==============================================================================
|
|
|
|
namespace choc::javascript
|
|
{
|
|
|
|
template <typename PrimitiveType>
|
|
PrimitiveType ArgumentList::get (size_t index, PrimitiveType defaultValue) const
|
|
{
|
|
if (auto a = (*this)[index])
|
|
return a->getWithDefault<PrimitiveType> (defaultValue);
|
|
|
|
return defaultValue;
|
|
}
|
|
|
|
struct Context::Pimpl
|
|
{
|
|
Pimpl() = default;
|
|
virtual ~Pimpl() = default;
|
|
|
|
virtual void registerFunction (const std::string&, NativeFunction) = 0;
|
|
virtual choc::value::Value evaluateExpression (const std::string&) = 0;
|
|
virtual void run (const std::string&, ReadModuleContentFn*, CompletionHandler) = 0;
|
|
virtual void prepareForCall (std::string_view, uint32_t numArgs) = 0;
|
|
virtual choc::value::Value performCall() = 0;
|
|
virtual void pushObjectOrArray (const choc::value::ValueView&) = 0;
|
|
virtual void pushArg (std::string_view) = 0;
|
|
virtual void pushArg (int32_t) = 0;
|
|
virtual void pushArg (int64_t) = 0;
|
|
virtual void pushArg (uint32_t) = 0;
|
|
virtual void pushArg (double) = 0;
|
|
virtual void pushArg (bool) = 0;
|
|
virtual void pumpMessageLoop() = 0;
|
|
|
|
void pushArg (const std::string& v) { pushArg (std::string_view (v)); }
|
|
void pushArg (const char* v) { pushArg (std::string_view (v)); }
|
|
void pushArg (uint64_t v) { pushArg (static_cast<int64_t> (v)); }
|
|
void pushArg (float v) { pushArg (static_cast<double> (v)); }
|
|
|
|
void pushArg (const choc::value::ValueView& v)
|
|
{
|
|
if (v.isInt32()) return pushArg (v.getInt32());
|
|
if (v.isInt64()) return pushArg (v.getInt64());
|
|
if (v.isFloat32()) return pushArg (v.getFloat32());
|
|
if (v.isFloat64()) return pushArg (v.getFloat64());
|
|
if (v.isString()) return pushArg (v.getString());
|
|
if (v.isBool()) return pushArg (v.getBool());
|
|
if (v.isVoid()) throw Error ("Function arguments cannot be void!");
|
|
|
|
pushObjectOrArray (v);
|
|
}
|
|
};
|
|
|
|
inline Context::Context (std::unique_ptr<Pimpl> p) : pimpl (std::move (p)) {}
|
|
inline Context::~Context() = default;
|
|
inline Context::Context (Context&&) = default;
|
|
inline Context& Context::operator= (Context&&) = default;
|
|
|
|
inline choc::value::Value Context::invoke (std::string_view functionName)
|
|
{
|
|
CHOC_ASSERT (pimpl != nullptr); // cannot call this on a moved-from context!
|
|
pimpl->prepareForCall (functionName, 0);
|
|
return pimpl->performCall();
|
|
}
|
|
|
|
template <typename... Args>
|
|
choc::value::Value Context::invoke (std::string_view functionName, Args&&... args)
|
|
{
|
|
CHOC_ASSERT (pimpl != nullptr); // cannot call this on a moved-from context!
|
|
pimpl->prepareForCall (functionName, sizeof...(args));
|
|
(pimpl->pushArg (std::forward<Args> (args)), ...);
|
|
return pimpl->performCall();
|
|
}
|
|
|
|
template <typename ArgList>
|
|
choc::value::Value Context::invokeWithArgList (std::string_view functionName, const ArgList& args)
|
|
{
|
|
CHOC_ASSERT (pimpl != nullptr); // cannot call this on a moved-from context!
|
|
pimpl->prepareForCall (functionName, static_cast<uint32_t> (args.size()));
|
|
|
|
for (auto& arg : args)
|
|
pimpl->pushArg (arg);
|
|
|
|
return pimpl->performCall();
|
|
}
|
|
|
|
inline void Context::registerFunction (const std::string& name, NativeFunction fn)
|
|
{
|
|
CHOC_ASSERT (pimpl != nullptr); // cannot call this on a moved-from context!
|
|
pimpl->registerFunction (name, std::move (fn));
|
|
}
|
|
|
|
inline choc::value::Value Context::evaluateExpression (const std::string& javascriptCode)
|
|
{
|
|
CHOC_ASSERT (pimpl != nullptr); // cannot call this on a moved-from context!
|
|
return pimpl->evaluateExpression (javascriptCode);
|
|
}
|
|
|
|
inline void Context::run (const std::string& javascriptCode, CompletionHandler handleResult)
|
|
{
|
|
CHOC_ASSERT (pimpl != nullptr); // cannot call this on a moved-from context!
|
|
pimpl->run (javascriptCode, nullptr, std::move (handleResult));
|
|
}
|
|
|
|
inline void Context::runModule (const std::string& moduleCode, ReadModuleContentFn readModule, CompletionHandler handleResult)
|
|
{
|
|
CHOC_ASSERT (pimpl != nullptr); // cannot call this on a moved-from context!
|
|
pimpl->run (moduleCode, std::addressof (readModule), std::move (handleResult));
|
|
}
|
|
|
|
inline void Context::pumpMessageLoop()
|
|
{
|
|
CHOC_ASSERT (pimpl != nullptr); // cannot call this on a moved-from context!
|
|
pimpl->pumpMessageLoop();
|
|
}
|
|
|
|
inline std::string makeSafeIdentifier (std::string s)
|
|
{
|
|
constexpr static std::string_view reservedWords[] =
|
|
{
|
|
"abstract", "arguments", "await", "boolean", "break", "byte", "case", "catch",
|
|
"char", "class", "const", "continue", "debugger", "default", "delete", "do",
|
|
"double", "else", "enum", "eval", "export", "extends", "false", "final",
|
|
"finally", "float", "for", "function", "goto", "if", "implements", "import",
|
|
"in", "instanceof", "int", "interface", "let", "long", "native", "new",
|
|
"null", "package", "private", "protected", "public", "return", "short", "static",
|
|
"super", "switch", "synchronized", "this", "throw", "throws", "transient", "true",
|
|
"try", "typeof", "var", "void", "volatile", "while", "with", "yield"
|
|
};
|
|
|
|
for (auto& c : s)
|
|
if (std::string_view (" ,./;:").find (c) != std::string_view::npos)
|
|
c = '_';
|
|
|
|
s.erase (std::remove_if (s.begin(), s.end(), [&] (char c)
|
|
{
|
|
return ! ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || (c >= '0' && c <= '9'));
|
|
}), s.end());
|
|
|
|
if (s[0] >= '0' && s[0] <= '9') // Identifiers can't start with a digit
|
|
s = "_" + s;
|
|
|
|
for (auto keyword : reservedWords)
|
|
if (s == keyword)
|
|
return s + "_";
|
|
|
|
return s;
|
|
}
|
|
|
|
} // namespace choc::javascript
|
|
|
|
#endif // CHOC_JAVASCRIPT_HEADER_INCLUDED
|
|
|
|
#ifdef CHOC_JAVASCRIPT_IMPLEMENTATION
|
|
// The way the javascript classes work has changed: instead of
|
|
// setting CHOC_JAVASCRIPT_IMPLEMENTATION in one of your source files, just
|
|
// include the actual engine that you want to use, e.g. choc_javascript_QuickJS.h
|
|
// in (only!) one of your source files, and use that to create instances of that engine.
|
|
#error "CHOC_JAVASCRIPT_IMPLEMENTATION is deprecated"
|
|
#endif
|