Home > OS >  Problem with returning booleans from C to JavaScript using v8 JS engine
Problem with returning booleans from C to JavaScript using v8 JS engine

Time:01-23

I am trying to embed v8 static libs into my standalone C application ( macOS: Monterey 12.6, CPU: Intel Core i7, Xcode: 13.1 (13A1030d) )

  1. I've written a simple bash script to get v8 static libs:
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=$PWD/depot_tools:$PATH

mkdir v8
cd v8
fetch v8
cd v8

git checkout branch-heads/9.4
gclient sync

gn gen out/x64.release--args='
is_component_build = false
is_debug = false
target_cpu = "x64"
use_goma = false
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_verify_heap = true
dcheck_always_on = false
v8_monolithic = true
v8_use_external_startup_data = false'

ninja -C out/x64.release v8_monolith
  1. copy libv8_monolith.a, libv8_libplatform.a, libv8_libbase.a from out/x64.release/obj to my xcode c console project's folder and link them to it.
  2. the c part of test project looks like:
#include <iostream>
#include <string>
#include <fstream>
#include <sstream>
#include <filesystem>

#include <v8.h>
#include <libplatform/libplatform.h>

using namespace v8;

std::string ToStdString(v8::Isolate *isolate, v8::MaybeLocal<v8::Value> valueMaybe);

bool LoadScript(const std::string& path, Isolate* _isolate, Local<Context> _context);

void LogCallback(const FunctionCallbackInfo<Value> &args);

void CreateTrue(const FunctionCallbackInfo<Value> &args) {
    v8::Isolate *isolate(args.GetIsolate());
    args.GetReturnValue().Set(v8::True(isolate));
}

void CreateFalse(const FunctionCallbackInfo<Value> &args) {
    v8::Isolate *isolate(args.GetIsolate());
    args.GetReturnValue().Set(v8::False(isolate));
}


int main(int argc, const char * argv[]) {
    std::unique_ptr<Platform> platform = v8::platform::NewDefaultPlatform();
    
    V8::InitializePlatform(platform.get());
    V8::Initialize();
    
    Isolate::CreateParams create_params;
    create_params.array_buffer_allocator = ArrayBuffer::Allocator::NewDefaultAllocator();
    Isolate* isolate = Isolate::New(create_params);
    
    Local<Context> context;
    {
        Locker l(isolate);
        Isolate::Scope isolate_scope(isolate);
        HandleScope handle_scope(isolate);
        
        Local<ObjectTemplate> global = ObjectTemplate::New(isolate);
        
        global->Set(isolate, "log", FunctionTemplate::New(isolate, LogCallback));
        
        global->Set(isolate, "cpp_CreateTrue", FunctionTemplate::New(isolate, CreateTrue));
        global->Set(isolate, "cpp_CreateFalse", FunctionTemplate::New(isolate, CreateFalse));
        
        context = Context::New(isolate,  nullptr, global);
        
        auto jsLikeBool = v8::Boolean::New(isolate, true);
        // at this point typeof this jsLikeBool equals to string, but should be equal to boolean
        if (jsLikeBool->IsBoolean()) {
            std::cout << "c  ::GetBoolean jsLikeBoolean has boolean type" << std::endl;
        } else {
            std::cout << "c  ::GetBoolean jsLikeBoolean DOES NOT have boolean type, but HAS: " << ToStdString(isolate, jsLikeBool->TypeOf(isolate)) << std::endl;
        }
    }
    
    if (!LoadScript(std::filesystem::current_path() / "assets" / "bools.js", isolate, context)) {
        return 1;
    }
    
    isolate->Dispose();
    V8::Dispose();
    V8::ShutdownPlatform();
    
    delete create_params.array_buffer_allocator;
    return 0;
}
  1. assets/bools.js file:
const Js_CreateTrue = () => true;
const Js_CreateFalse = () => false;

const CheckCreator = function(tag, getter, expectedType, expectedValue) {
    const value = getter();
    const valueType = typeof value;
    
    if (valueType !== expectedType) {
        log(`JS::test ${tag} failed types check, actual: ${valueType}, but expected: ${expectedType}`);
    }
        
    if (value !== expectedValue) {
        log(`JS::test ${tag} failed values check, actual: ${value}, but expected: ${expectedValue}`);
    }
}

CheckCreator("Js_CreateTrue", Js_CreateTrue, "boolean", true); // OK
CheckCreator("Js_CreateFalse", Js_CreateFalse, "boolean", false); // OK

CheckCreator("cpp_CreateTrue", cpp_CreateTrue, "boolean", true); // FAILED: gets an empty string
CheckCreator("cpp_CreateFalse", cpp_CreateFalse, "boolean", false); // causes crash in c  , actual type is `symbol`

I have already tried different tags/branches of v8 sources, nothing helps. How to get rid of such strange behaviour with returning booleans from C to JS?

CodePudding user response:

I will look at the whole question carefully a bit later. As for the specific question in the title see bellow.

v8::ReturnValue<T> has a member function void Set(bool value). The right way to set a boolean to a return value is

void CreateFalse(const FunctionCallbackInfo<Value> &args) {
  args.GetReturnValue().Set(false);
}

void CreateTrue(const FunctionCallbackInfo<Value> &args) {
  args.GetReturnValue().Set(true);
}

CodePudding user response:

I can't repro the same symptoms you're seeing, but maybe close enough...

First off, I'm somewhat annoyed by the fact that the example code is incomplete. What on earth was the point of the edit where you dropped the bodies of LoadScript and the other helper functions? Are you trying to make it difficult to help you?

You also left out one crucial step, which is how exactly you compiled your embedding application. My first guess was that you may have an inconsistency in where the -DV8_COMPRESS_POINTERS flag is passed; but on closer investigation that's probably not the problem, because that would have resulted in a very hard-to-miss error message on startup, even with a version as old as 9.4.
That said, there's a number of other inconsistencies that could be happening there. For example, you only said that you're copying the compiled libraries, you didn't confirm that you're #including the v8.h from the checkout.

When I try your code (restored from before it was edited out), I see c ::GetBoolean jsLikeBoolean has boolean type as expected.

However, I'm getting a segfault after that message. The following sequence is a bug:

Local<Context> context;
{
  HandleScope handle_scope(isolate);
  context = Context::New(...);
}
LoadScript(..., context);

When the HandleScope goes out of scope, it destroys all handles created in it, which includes context. The fix is to perform the LoadScript call while the HandleScope is still alive. Or turn context into a v8::Global.


Side notes:

  • GetNewContext doesn't do what it says. In its current form, it only creates a new handle for the existing context, which serves no purpose. (Creating an EscapableHandleScope just for returning one single handle is also rather pointless. That'd only be useful if there was a non-trivial number of temporary handles that could legitimately go out of scope when the helper function returns.)
  • When starting a new project in 2023, I wouldn't use a JS engine from 2021.
  • Your v8_enable_* build flags are surprising. Do you really want debugging features in a non-debug build? A good set of build flag is what you get when you follow the documentation:
dcheck_always_on = false
is_component_build = false
is_debug = false
target_cpu = "x64"  # Or "arm64" if that's your hardware.
use_custom_libcxx = false
v8_monolithic = true
v8_use_external_startup_data = false
# As of V8 10.8, if you don't want to bother building sandboxing
# support into your embedder:
v8_enable_sandbox = false
  • With up-to-date V8 versions, your code will need a few minor adaptations: ShutdownPlatform is now called DisposePlatform, and the ScriptOrigin constructor takes an Isolate* parameter.
  • Related