diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4999d3e9..2db24a5f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,13 @@ jobs: cxx: clang++ type: shared backend: JavaScriptCore + # TODO(RaisinTen): Support other OSs and static configuration for + # the V8 backend. + - os: macos-latest + cc: clang + cxx: clang++ + type: shared + backend: V8 - os: ubuntu-latest cc: clang cxx: clang++ diff --git a/Brewfile b/Brewfile index 97725f39..92ae3cd0 100644 --- a/Brewfile +++ b/Brewfile @@ -1,3 +1,4 @@ brew "cmake" brew "clang-format" brew "doxygen" +brew "v8" diff --git a/Makefile b/Makefile index 2fc14985..27ee24da 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,7 @@ CTEST = ctest # Options PRESET = Debug SHARED = OFF +BACKEND = JavaScriptCore all: configure compile test @@ -12,7 +13,7 @@ configure: .always $(CMAKE) -S . -B ./build \ -DCMAKE_BUILD_TYPE:STRING=$(PRESET) \ -DCMAKE_COMPILE_WARNING_AS_ERROR:BOOL=ON \ - -DINCLUDEJS_BACKEND:STRING=JavaScriptCore \ + -DINCLUDEJS_BACKEND:STRING=$(BACKEND) \ -DINCLUDEJS_ENGINE:BOOL=ON \ -DINCLUDEJS_TESTS:BOOL=ON \ -DINCLUDEJS_DOCS:BOOL=ON \ diff --git a/cmake/FindV8.cmake b/cmake/FindV8.cmake new file mode 100644 index 00000000..6621bbc6 --- /dev/null +++ b/cmake/FindV8.cmake @@ -0,0 +1,22 @@ +if(NOT V8_FOUND) + if(APPLE) + add_library(v8 INTERFACE IMPORTED) + # Since the "includejs" library is compiled with the `-isysroot` flag of + # Clang, it is unable to find the V8 library and headers which have been + # downloaded from Homebrew. + # Refs: https://github.com/Homebrew/homebrew-core/issues/45061#issuecomment-541420664 + set(V8_VERSION "11.4.183.25") + target_include_directories(v8 + INTERFACE "/usr/local/Cellar/v8/${V8_VERSION}/include") + target_link_directories(v8 + INTERFACE "/usr/local/Cellar/v8/${V8_VERSION}/lib") + target_link_libraries(v8 INTERFACE "-lv8") + target_link_libraries(v8 INTERFACE "-lv8_libplatform") + target_compile_definitions(v8 + INTERFACE V8_COMPRESS_POINTERS) + target_compile_definitions(v8 + INTERFACE V8_ENABLE_SANDBOX) + add_library(V8::V8 ALIAS v8) + set(V8_FOUND ON) + endif() +endif() diff --git a/src/engine/CMakeLists.txt b/src/engine/CMakeLists.txt index 5c66d03e..1d4f5fd9 100644 --- a/src/engine/CMakeLists.txt +++ b/src/engine/CMakeLists.txt @@ -8,6 +8,8 @@ if(INCLUDEJS_BACKEND STREQUAL "JavaScriptCore") list(APPEND INCLUDEJS_ENGINE_SOURCES javascript_core/engine_context.cc) list(APPEND INCLUDEJS_ENGINE_SOURCES javascript_core/engine_promise.cc) list(APPEND INCLUDEJS_ENGINE_SOURCES javascript_core/engine_value.cc) +elseif(INCLUDEJS_BACKEND STREQUAL "V8") + list(APPEND INCLUDEJS_ENGINE_SOURCES v8/engine.cc) else() message(FATAL_ERROR "Unknown IncludeJS backend: ${INCLUDEJS_BACKEND}") endif() @@ -24,6 +26,11 @@ if(INCLUDEJS_INSTALL) "${PROJECT_SOURCE_DIR}/cmake/FindJavaScriptCore.cmake" DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/includejs" COMPONENT includejs_dev) + elseif(INCLUDEJS_BACKEND STREQUAL "V8") + install(FILES + "${PROJECT_SOURCE_DIR}/cmake/FindV8.cmake" + DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/includejs" + COMPONENT includejs_dev) endif() endif() @@ -35,6 +42,12 @@ if(INCLUDEJS_BACKEND STREQUAL "JavaScriptCore") PRIVATE JavaScriptCore::JavaScriptCore) target_compile_definitions(includejs_engine PUBLIC SOURCEMETA_INCLUDEJS_ENGINE_JAVASCRIPT_CORE) +elseif(INCLUDEJS_BACKEND STREQUAL "V8") + find_package(V8 REQUIRED) + target_link_libraries(includejs_engine + PRIVATE V8::V8) + target_compile_definitions(includejs_engine + PUBLIC SOURCEMETA_INCLUDEJS_ENGINE_V8) else() message(FATAL_ERROR "Unknown IncludeJS backend: ${INCLUDEJS_BACKEND}") endif() diff --git a/src/engine/include/includejs/engine.h b/src/engine/include/includejs/engine.h index 802f0503..fc789233 100644 --- a/src/engine/include/includejs/engine.h +++ b/src/engine/include/includejs/engine.h @@ -35,8 +35,11 @@ class INCLUDEJS_ENGINE_EXPORT Engine { auto evaluate(std::ifstream &stream, const std::filesystem::path &path) -> Value; + // TODO(RaisinTen): Add support for bind_function() to the V8 backend. +#if !defined(SOURCEMETA_INCLUDEJS_ENGINE_V8) auto bind_function(std::initializer_list location, Function function) -> void; +#endif auto bind_global(std::initializer_list location, Value value) -> void; auto bind_global(std::initializer_list location, bool value) diff --git a/src/engine/v8/engine.cc b/src/engine/v8/engine.cc new file mode 100644 index 00000000..21623293 --- /dev/null +++ b/src/engine/v8/engine.cc @@ -0,0 +1,95 @@ +#include + +#include + +#include +#include + +namespace sourcemeta { +namespace includejs { + +struct Engine::Internal {}; + +Engine::Engine() : internal{std::make_unique()} { + // TODO(RaisinTen): This code is here to make it easy to verify if the V8 + // integration is working. + + // This example code has been taken from: + // https://chromium.googlesource.com/v8/v8/+/branch-heads/11.9/samples/hello-world.cc + + // Initialize V8. + std::unique_ptr platform = v8::platform::NewDefaultPlatform(); + v8::V8::InitializePlatform(platform.get()); + v8::V8::Initialize(); + + // Create a new Isolate and make it the current one. + v8::Isolate::CreateParams create_params; + create_params.array_buffer_allocator = + v8::ArrayBuffer::Allocator::NewDefaultAllocator(); + v8::Isolate *isolate = v8::Isolate::New(create_params); + { + v8::Isolate::Scope isolate_scope(isolate); + // Create a stack-allocated handle scope. + v8::HandleScope handle_scope(isolate); + // Create a new context. + v8::Local context = v8::Context::New(isolate); + // Enter the context for compiling and running the hello world script. + v8::Context::Scope context_scope(context); + { + // Create a string containing the JavaScript source code. + v8::Local source = + v8::String::NewFromUtf8Literal(isolate, "'Hello' + ', World!'"); + // Compile the source code. + v8::Local script = + v8::Script::Compile(context, source).ToLocalChecked(); + // Run the script to get the result. + v8::Local result = script->Run(context).ToLocalChecked(); + // Convert the result to an UTF8 string and print it. + v8::String::Utf8Value utf8(isolate, result); + printf("%s\n", *utf8); + } + { + // Use the JavaScript API to generate a WebAssembly module. + // + // |bytes| contains the binary format for the following module: + // + // (func (export "add") (param i32 i32) (result i32) + // get_local 0 + // get_local 1 + // i32.add) + // + const char csource[] = R"( + let bytes = new Uint8Array([ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x07, 0x01, + 0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00, 0x07, + 0x07, 0x01, 0x03, 0x61, 0x64, 0x64, 0x00, 0x00, 0x0a, 0x09, 0x01, + 0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a, 0x0b + ]); + let module = new WebAssembly.Module(bytes); + let instance = new WebAssembly.Instance(module); + instance.exports.add(3, 4); + )"; + // Create a string containing the JavaScript source code. + v8::Local source = + v8::String::NewFromUtf8Literal(isolate, csource); + // Compile the source code. + v8::Local script = + v8::Script::Compile(context, source).ToLocalChecked(); + // Run the script to get the result. + v8::Local result = script->Run(context).ToLocalChecked(); + // Convert the result to a uint32 and print it. + uint32_t number = result->Uint32Value(context).ToChecked(); + printf("3 + 4 = %u\n", number); + } + } + // Dispose the isolate and tear down V8. + isolate->Dispose(); + v8::V8::Dispose(); + v8::V8::DisposePlatform(); + delete create_params.array_buffer_allocator; +} + +Engine::~Engine() {} + +} // namespace includejs +} // namespace sourcemeta diff --git a/test/engine/CMakeLists.txt b/test/engine/CMakeLists.txt index a3601d8c..e28c8615 100644 --- a/test/engine/CMakeLists.txt +++ b/test/engine/CMakeLists.txt @@ -1,3 +1,8 @@ +if(INCLUDEJS_BACKEND STREQUAL "V8") + # TODO(RaisinTen): Start testing the V8 backend. + return() +endif() + add_executable(includejs_engine_unit engine_binding_test.cc engine_evaluate_test.cc diff --git a/test/packaging/find_package/hello.cc b/test/packaging/find_package/hello.cc index 69488c9e..cd221632 100644 --- a/test/packaging/find_package/hello.cc +++ b/test/packaging/find_package/hello.cc @@ -6,8 +6,11 @@ auto main() -> int { sourcemeta::includejs::Engine engine; + // TODO(RaisinTen): Remove this once V8 supports this. +#if !defined(SOURCEMETA_INCLUDEJS_ENGINE_V8) sourcemeta::includejs::Value result{engine.evaluate("1 + 3", "index.js")}; assert(result.is_number()); std::cout << result.to_number() << "\n"; +#endif return EXIT_SUCCESS; }