view C++/modules/Js/Js.h @ 397:6b2db5425836

Js: initial support for objects as shared_ptr
author David Demelier <markand@malikania.fr>
date Tue, 29 Sep 2015 12:50:28 +0200
parents 69adcefe73ae
children 94bfe7ba9a13
line wrap: on
line source

/*
 * Js.h -- JavaScript wrapper for Duktape
 *
 * Copyright (c) 2013, 2014, 2015 David Demelier <markand@malikania.fr>
 *
 * 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 _JS_H_
#define _JS_H_

/**
 * @file Js.h
 * @brief Bring JavaScript using Duktape
 *
 * This file provides usual Duktape function renamed and placed into `js` namespace. It also replaces error
 * code with exceptions when possible.
 *
 * For convenience, this file also provides templated functions, overloads and much more.
 */

#include <cassert>
#include <cerrno>
#include <cstring>
#include <memory>
#include <stack>
#include <string>
#include <vector>

#include <duktape.h>

namespace js {

#if !defined(NDEBUG)
#define assertBegin(ctx)						\
	int _topstack = duk_get_top(ctx)
#else
#define assertBegin(ctx)
#endif

#if !defined(NDEBUG)
#define assertEquals(ctx)						\
	assert(_topstack == duk_get_top(ctx))
#else
#define assertEquals(ctx)
#endif

#if !defined(NDEBUG)
#define assertEnd(ctx, count)					\
	assert(_topstack == (duk_get_top(ctx) - count))
#else
#define assertEnd(ctx, count)
#endif

/**
 * Typedef for readability.
 */
using ContextPtr = duk_context *;

/**
 * @class Object
 * @brief Empty class tag for push() function
 */
class Object {
};

/**
 * @class Arary
 * @brief Empty class tag for push() function
 */
class Array {
};

/**
 * @class Function
 * @brief Duktape/C function definition
 */
class Function {
public:
	/**
	 * The function pointer, must not be null.
	 */
	duk_c_function function{nullptr};

	/**
	 * Number of args that the function takes
	 */
	duk_idx_t nargs{0};
};

/**
 * @class ErrorInfo
 * @brief Error description
 *
 * This class fills the fields got in an Error object.
 */
class ErrorInfo : public std::exception {
public:
	std::string name;		//!< name of error
	std::string message;		//!< error message
	std::string stack;		//!< stack if available
	std::string fileName;		//!< filename if applicable
	int lineNumber{0};		//!< line number if applicable

	/**
	 * Get the error message. This effectively returns message field.
	 *
	 * @return the message
	 */
	const char *what() const noexcept override
	{
		return message.c_str();
	}
};

template <typename Type>
class TypeInfo {
public:
};

/**
 * @class Context
 * @brief RAII based Duktape handler
 *
 * This class is implicitly convertible to duk_context for convenience.
 */
class Context {
private:
	using Deleter = void (*)(duk_context *);
	using Handle = std::unique_ptr<duk_context, Deleter>;

	Handle m_handle;

	/* Move and copy forbidden */
	Context(const Context &) = delete;
	Context &operator=(const Context &) = delete;
	Context(const Context &&) = delete;
	Context &operator=(const Context &&) = delete;


public:
	/**
	 * Create default context.
	 */
	inline Context()
		: m_handle{duk_create_heap_default(), duk_destroy_heap}
	{
	}

	/**
	 * Create borrowed context that will not be deleted.
	 *
	 * @param ctx the pointer to duk_context
	 */
	inline Context(ContextPtr ctx) noexcept
		: m_handle{ctx, [] (ContextPtr) {}}
	{
	}

	/**
	 * Convert the context to the native Duktape/C type.
	 *
	 * @return the duk_context
	 */
	inline operator duk_context *() noexcept
	{
		return m_handle.get();
	}

	/**
	 * Convert the context to the native Duktape/C type.
	 *
	 * @return the duk_context
	 */
	inline operator duk_context *() const noexcept
	{
		return m_handle.get();
	}

	/* ----------------------------------------------------------
	 * Push / Get / Require
	 * ---------------------------------------------------------- */

	/**
	 * Push a value into the stack. Calls TypeInfo<T>::push(*this, value);
	 *
	 * @param value the value to forward
	 */
	template <typename Type>
	inline void push(Type &&value)
	{
		TypeInfo<std::decay_t<Type>>::push(*this, std::forward<Type>(value));
	}

	/**
	 * Generic template function to get a value from the stack.
	 *
	 * @param index the index
	 * @return the value
	 */
	template <typename Type>
	inline auto get(duk_idx_t index) -> decltype(TypeInfo<Type>::get(*this, 0))
	{
		return TypeInfo<Type>::get(*this, index);
	}

	template <typename Type>
	inline auto require(duk_idx_t index) -> decltype(TypeInfo<Type>::require(*this, 0))
	{
		return TypeInfo<Type>::require(*this, index);
	}

	/* --------------------------------------------------------
	 * Get functions (for object)
	 * -------------------------------------------------------- */

	template <typename Type>
	inline auto getObject(duk_idx_t index, const std::string &name) -> decltype(get<Type>(0))
	{
		assertBegin(m_handle.get());
		duk_get_prop_string(m_handle.get(), index, name.c_str());
		auto &&value = get<Type>(-1);
		duk_pop(m_handle.get());
		assertEquals(m_handle.get());

		return value;
	}

	/* --------------------------------------------------------
	 * Set functions (for object)
	 * -------------------------------------------------------- */

	template <typename Type>
	void setObject(duk_idx_t index, const std::string &name, Type&& value)
	{
		index = duk_normalize_index(m_handle.get(), index);

		assertBegin(m_handle.get());
		push(std::forward<Type>(value));
		duk_put_prop_string(m_handle.get(), index, name.c_str());
		assertEquals(m_handle.get());
	}

	/* --------------------------------------------------------
	 * Basic functions
	 * -------------------------------------------------------- */

	/**
	 * Get the type of the value at the specified index.
	 *
	 * @param ctx the context
	 * @param index the idnex
	 * @return the type
	 */
	duk_int_t type(duk_idx_t index);

	/**
	 * Get the current stack size.
	 *
	 * @param ctx the context
	 * @return the stack size
	 */
	int top();

	/**
	 * Pop a certain number of values from the top of the stack.
	 *
	 * @param ctx the context
	 * @param count the number of values to pop
	 * @pre count must be positive
	 */
	void pop(int count = 1);

	/**
	 * Check if idx1 is an instance of idx2.
	 *
	 * @param ctx the context
	 * @param idx1 the value to test
	 * @param idx2 the instance requested
	 * @return true if idx1 is instance of idx2
	 */
	duk_bool_t instanceof(duk_idx_t idx1, duk_idx_t idx2);

	/**
	 * Call the object at the top of the stack.
	 *
	 * @param ctx the context
	 * @param nargs the number of arguments
	 * @note Non-protected
	 */
	void call(duk_idx_t nargs = 0);

	/**
	 * Call in protected mode the object at the top of the stack.
	 *
	 * @param ctx the context
	 * @param nargs the number of arguments
	 * @throw ErrorInfo on errors
	 */
	void pcall(duk_idx_t nargs = 0);

	/* ------------------------------------------------------------------
	 * Eval functions
	 * ------------------------------------------------------------------ */

	/**
	 * Evaluate a non-protected chunk at the top of the stack.
	 *
	 * @param ctx
	 */
	void eval();

	/**
	 * Evaluate a non-protected string script.
	 *
	 * @param ctx the context
	 * @param script the script content
	 */
	void evalString(const std::string &script);

	/**
	 * Evaluate a non-protected file.
	 *
	 * @param ctx the context
	 * @param file the file
	 */
	void evalFile(const std::string &file);

	/**
	 * Evaluate a protected chunk.
	 *
	 * @param ctx the context
	 * @throw ErrorInfo the error
	 */
	void peval();

	/**
	 * Evaluate a protected string script.
	 *
	 * @param ctx the context
	 * @param script the script content
	 * @throw ErrorInfo the error
	 */
	void pevalString(const std::string &script);

	/**
	 * Evaluate a protected file.
	 *
	 * @param ctx the context
	 * @param file the file
	 * @throw ErrorInfo the error
	 */
	void pevalFile(const std::string &file);

	/* ------------------------------------------------------------------
	 * Extended functions
	 * ------------------------------------------------------------------ */

	/**
	 * Enumerate an object or an array at the specified index.
	 *
	 * @param ctx the context
	 * @param index the object or array index
	 * @param flags the optional flags to pass to duk_enum
	 * @param getvalue set to true if you want to extract the value
	 * @param func the function to call for each properties
	 */
	template <typename Func>
	void enumerate(duk_idx_t index, duk_uint_t flags, duk_bool_t getvalue, Func&& func)
	{
		assertBegin(m_handle.get());
		duk_enum(m_handle.get(), index, flags);

		while (duk_next(m_handle.get(), -1, getvalue)) {
			func(*this);
			duk_pop_n(m_handle.get(), 1 + (getvalue ? 1 : 0));
		}

		duk_pop(m_handle.get());
		assertEquals(m_handle.get());
	}

	/* --------------------------------------------------------
	 * Global functions
	 * -------------------------------------------------------- */

	/**
	 * Get a global value.
	 *
	 * @param ctx the context
	 * @param name the name of the global variable
	 * @return the value
	 */
	template <typename Type>
	inline auto getGlobal(const std::string &name) -> decltype(get<Type>(0))
	{
		assertBegin(m_handle.get());
		duk_get_global_string(m_handle.get(), name.c_str());
		auto &&value = get<Type>(-1);
		duk_pop(m_handle.get());
		assertEquals(m_handle.get());

		return value;
	}

	/**
	 * Set a global variable.
	 *
	 * @param ctx the context
	 * @param name the name of the global variable
	 * @param type the value to set
	 */
	template <typename Type>
	inline void setGlobal(const std::string &name, Type&& type)
	{
		assertBegin(m_handle.get());
		push(std::forward<Type>(type));
		duk_put_global_string(m_handle.get(), name.c_str());
		assertEquals(m_handle.get());
	}

	/**
	 * Throw an ECMAScript exception.
	 *
	 * @param ctx the context
	 * @param ex the exception
	 */
	template <typename Exception>
	void raise(const Exception &ex)
	{
		ex.create(m_handle.get());

		duk_push_string(m_handle.get(), ex.name().c_str());
		duk_put_prop_string(m_handle.get(), -2, "name");
		duk_throw(m_handle.get());
	}

	template <typename T>
	inline auto self() -> decltype(TypeInfo<T>::get(*this, 0))
	{
		duk_push_this(m_handle.get());
		auto &&value = TypeInfo<T>::get(*this, -1);
		duk_pop(m_handle.get());

		return value;
	}
};

/* ------------------------------------------------------------------
 * Exception handling
 * ------------------------------------------------------------------ */

/**
 * @class ExceptionAbstract
 * @brief Base class for standard ECMAScript exceptions
 * @warning Override the function create for your own exceptions
 */
class ExceptionAbstract {
protected:
	std::string m_name;
	std::string m_message;

public:
	/**
	 * Construct an exception of type name with the specified message.
	 *
	 * @param name the name (e.g TypeError)
	 * @param message the message
	 */
	ExceptionAbstract(std::string name, std::string message);

	/**
	 * Get the exception type name.
	 *
	 * @return the exception type
	 */
	const std::string &name() const noexcept;

	/**
	 * Create the exception on the stack.
	 *
	 * @note the default implementation search for the global variables
	 * @param ctx the context
	 */
	virtual void create(ContextPtr ctx) const noexcept;
};

/**
 * @class Error
 * @brief Base ECMAScript error class
 */
class Error : public ExceptionAbstract {
public:
	Error(std::string message);
};

/**
 * @class EvalError
 * @brief Error in eval() function
 */
class EvalError : public ExceptionAbstract {
public:
	EvalError(std::string message);
};

/**
 * @class RangeError
 * @brief Value is out of range
 */
class RangeError : public ExceptionAbstract {
public:
	RangeError(std::string message);
};

/**
 * @class ReferenceError
 * @brief Trying to use a variable that does not exist
 */
class ReferenceError : public ExceptionAbstract {
public:
	ReferenceError(std::string message);
};

/**
 * @class SyntaxError
 * @brief Syntax error in the script
 */
class SyntaxError : public ExceptionAbstract {
public:
	SyntaxError(std::string message);
};

/**
 * @class TypeError
 * @brief Invalid type given
 */
class TypeError : public ExceptionAbstract {
public:
	TypeError(std::string message);
};

/**
 * @class URIError
 * @brief URI manipulation failure
 */
class URIError : public ExceptionAbstract {
public:
	URIError(std::string message);
};

/* ------------------------------------------------------------------
 * Standard overloads for TypeInfo<T>::get
 * ------------------------------------------------------------------ */

template <>
class TypeInfo<int> {
public:
	static int get(ContextPtr ctx, duk_idx_t index)
	{
		return duk_get_int(ctx, index);
	}

	static void push(ContextPtr ctx, int value)
	{
		duk_push_int(ctx, value);
	}
};

template <>
class TypeInfo<bool> {
public:
	static bool get(Context &ctx, duk_idx_t index)
	{
		return duk_get_boolean(ctx, index);
	}

	static void push(Context &ctx, bool value)
	{
		duk_push_boolean(ctx, value);
	}
};

template <>
class TypeInfo<double> {
public:
	static double get(Context &ctx, duk_idx_t index)
	{
		return duk_get_number(ctx, index);
	}

	static void push(Context &ctx, double value)
	{
		duk_push_number(ctx, value);
	}
};

template <>
class TypeInfo<std::string> {
public:
	static std::string get(Context &ctx, duk_idx_t index)
	{
		duk_size_t size;
		const char *text = duk_get_lstring(ctx, index, &size);

		return std::string{text, size};
	}

	static void push(Context &ctx, const std::string &value)
	{
		duk_push_lstring(ctx, value.c_str(), value.length());
	}
};

template <>
class TypeInfo<const char *> {
public:
	static void push(Context &ctx, const char *value)
	{
		duk_push_string(ctx, value);
	}
};

template <>
class TypeInfo<Function> {
public:
	static void push(Context &ctx, const Function &fn)
	{
		assert(fn.function);

		duk_push_c_function(ctx, fn.function, fn.nargs);
	}
};

template <>
class TypeInfo<Object> {
public:
	static void push(Context &ctx, const Object &)
	{
		duk_push_object(ctx);
	}
};

template <>
class TypeInfo<Array> {
public:
	static void push(Context &ctx, const Array &)
	{
		duk_push_array(ctx);
	}
};

/* ------------------------------------------------------------------
 * Helpers for pointers and std::shared_ptr
 * ------------------------------------------------------------------ */

/**
 * @class TypeInfoShared
 * @brief Generates push / get / require for std::shared_ptr<T>
 *
 * Specialize TypeInfo<std::shared_ptr<T>> and inherits from this class to implement the push(),
 * get() and require() functions.
 *
 * You only need to implement `static void prototype(Context &ctx)` which must push the prototype
 * to use for the underlying object.
 */
template <typename T>
class TypeInfoShared {
public:
	static void push(Context &ctx, std::shared_ptr<T> value);
	static std::shared_ptr<T> get(Context &ctx, duk_idx_t index);
};

template <typename T>
void TypeInfoShared<T>::push(Context &ctx, std::shared_ptr<T> value)
{
	duk_push_object(ctx);
	duk_push_boolean(ctx, false);
	duk_put_prop_string(ctx, -2, "\xff""\xff""js-deleted");
	duk_push_pointer(ctx, new std::shared_ptr<T>(value));
	duk_put_prop_string(ctx, -2, "\xff""\xff""js-shared-ptr");

	TypeInfo<std::shared_ptr<T>>::prototype(ctx);

	// TODO: set deleter

	duk_set_prototype(ctx, -2);
}

template <typename T>
std::shared_ptr<T> TypeInfoShared<T>::get(Context &ctx, duk_idx_t index)
{
	duk_get_prop_string(ctx, index, "\xff""\xff""js-shared-ptr");
	std::shared_ptr<T> value = *static_cast<std::shared_ptr<T> *>(duk_to_pointer(ctx, -1));
	duk_pop(ctx);

	return value;
}

} // !js

#endif // !_JS_H_