diff --git a/src/engine/include/includejs/engine_value.h b/src/engine/include/includejs/engine_value.h index 01cf0d68..4fd3ca68 100644 --- a/src/engine/include/includejs/engine_value.h +++ b/src/engine/include/includejs/engine_value.h @@ -41,6 +41,7 @@ class INCLUDEJS_ENGINE_EXPORT Value { auto to_number() const -> double; auto to_string() const -> std::string; auto to_boolean() const -> bool; + auto to_function() const -> Function; auto at(const std::string &property) const -> std::optional; auto at(const unsigned int &position) const -> std::optional; auto set(const std::string &property, Value value) -> void; diff --git a/src/engine/javascript_core/engine_value.cc b/src/engine/javascript_core/engine_value.cc index caf2b227..f70009a7 100644 --- a/src/engine/javascript_core/engine_value.cc +++ b/src/engine/javascript_core/engine_value.cc @@ -161,6 +161,31 @@ auto Value::to_boolean() const -> bool { return JSValueToBoolean(this->internal->context, this->internal->value); } +auto Value::to_function() const -> Function { + assert(is_function()); + + return [context = this->internal->context, + value = this->internal->value](std::vector args) { + JSValueRef exception = nullptr; + JSObjectRef func = JSValueToObject(context, value, &exception); + assert(!exception); + assert(func != nullptr); + + std::vector js_args; + js_args.reserve(args.size()); + for (Value &arg : args) { + js_args.push_back(static_cast(arg.native())); + } + + // Now we can call the function, get the result and return through + // the Value class. + JSValueRef result = JSObjectCallAsFunction( + context, func, nullptr, js_args.size(), js_args.data(), nullptr); + assert(result != nullptr); + return Value{context, result}; + }; +} + auto Value::at(const std::string &property) const -> std::optional { assert(is_object()); JSValueRef exception = nullptr; @@ -179,6 +204,15 @@ auto Value::at(const std::string &property) const -> std::optional { property_string, &exception); assert(!exception); JSStringRelease(property_string); + + // The function could not be called outside of this object context + // as the C++ function store is tied to the object. + // This is why we don't support function properties. + if (JSObjectIsFunction( + this->internal->context, + JSValueToObject(this->internal->context, result, &exception))) { + throw std::runtime_error{"Function properties are not supported"}; + } return std::make_optional(this->internal->context, result); } diff --git a/test/engine/engine_value_function_test.cc b/test/engine/engine_value_function_test.cc index 8726690a..6b383df9 100644 --- a/test/engine/engine_value_function_test.cc +++ b/test/engine/engine_value_function_test.cc @@ -5,16 +5,19 @@ TEST(IncludeJS_Engine, is_function) { sourcemeta::includejs::Engine engine; - auto obj = engine.context().make_object(); - obj.set( - "foo", - [](std::vector arguments) - -> sourcemeta::includejs::Value { return std::move(arguments[0]); }); - obj.set("bar", engine.context().from(42)); - - EXPECT_TRUE(obj.at("foo").has_value()); - EXPECT_TRUE(obj.at("foo").value().is_function()); - - EXPECT_TRUE(obj.at("bar").has_value()); - EXPECT_FALSE(obj.at("bar").value().is_function()); + auto result = engine.evaluate("(function a() {})", "index.js"); + + EXPECT_TRUE(result.is_function()); +} + +TEST(IncludeJS_Engine, to_function) { + sourcemeta::includejs::Engine engine; + + auto result = engine.evaluate("(function a() { return 42; })", "index.js"); + + EXPECT_TRUE(result.is_function()); + + auto value = result.to_function()({}); + EXPECT_TRUE(value.is_number()); + EXPECT_EQ(value.to_number(), 42); } diff --git a/test/engine/engine_value_object_test.cc b/test/engine/engine_value_object_test.cc index 283349fa..ebb5667f 100644 --- a/test/engine/engine_value_object_test.cc +++ b/test/engine/engine_value_object_test.cc @@ -63,3 +63,14 @@ TEST(IncludeJS_Engine, set_object_function) { EXPECT_TRUE(result2.is_string()); EXPECT_EQ(result2.to_string(), "bar"); } + +TEST(IncludeJS_Engine, at_throw_for_functions) { + sourcemeta::includejs::Engine engine; + sourcemeta::includejs::Context &context = engine.context(); + + auto obj = context.make_object(); + obj.set("woo", + [](std::vector args) + -> sourcemeta::includejs::Value { return std::move(args[0]); }); + EXPECT_THROW(obj.at("woo"), std::runtime_error); +}