Search code examples
c++inlinev8

V8 lib and C++ inlining behavior differs from expectations and reproducing in separate project


I'm getting an error trying to construct a v8::ScriptOrigin object. Confirming the compiler error, my IDE only resolves the implicit copy and move constructors.

v8_api.cc: error: no matching constructor for initialization of 'v8::ScriptOrigin'

v8::ScriptOrigin::ScriptOrigin is marked with V8_INLINE macro which specifies inline and always_inline for the constructor.

I'm building my project with CMake and Apple clang version 13.1.6 and using an embedded v8 with args.gn:

v8_static_library=true
v8_monolithic=true
v8_use_external_startup_data=false
is_component_build=false
use_custom_libcxx=false

If I change v8config.h to define V8_INLINE as an empty string, I still can't resolve v8::ScriptOrigin::ScriptOrigin(...).

When I run nm I get:

libv8_monolith.a:v8-inspector-impl.o: 0000000000001070 0000000000000000 T v8::ScriptOrigin::ScriptOrigin(v8::Isolate*, v8::Local<v8::Value>, int, int, bool, int, v8::Local<v8::Value>, bool, bool, bool, v8::Local<v8::Data>)
libv8_monolith.a:v8-inspector-impl.o: 00000000000070a0 0000000000000000 T v8::ScriptOrigin::ScriptOrigin(v8::Isolate*, v8::Local<v8::Value>, int, int, bool, int, v8::Local<v8::Value>, bool, bool, bool, v8::Local<v8::Data>)

It looks like it's exporting a symbol twice for each object that uses it.

I get the same nm result when I build libv8_monolith.a with args.gn v8_no_inline=true, or without it or if I replace the #define V8_INLINE blocks with #define V8_INLINE inline or #define V8_INLINE /*not inlining*/ or delete V8_INLINE from ScriptOrigin.

I'm stumped. Why is it being exported as a symbol whether I build with v8_no_inline or not and why is it being exported twice for each comp unit that uses it?

I should be able to modify the v8-message.h header file to not be inline and then I could resolve the constructor from my codebase. However, I'm guessing it wouldn't link against it because there's duplicate symbols in the lib.

Because I've never used inline I created a sandbox project and (consistent with the docs I've read) confirmed that inlining prevents an external symbol and linking from another object. Although, I'm only seeing that with always_inline attribute and finding that the compiler chooses not to inline when I only use the inline keyword. However, the v8 lib differs from the sample project because the v8 lib exports two symbols for each object that references v8::ScriptOrigin.

How does this API work within the v8 codebase, or from other embedders like node.js or sample v8 code I've seen demoing the ScriptOrigin?

#include "v8/include/libplatform/libplatform.h"
#include "v8/include/v8-initialization.h"
#include "v8/include/v8-message.h"

int main([[maybe_unused]] int argc, char *argv[]) {
    v8::V8::InitializeICUDefaultLocation(argv[0]);
    v8::V8::InitializeExternalStartupData(argv[0]);
    std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();
    v8::V8::InitializePlatform(platform.get());
    v8::V8::Initialize();

    v8::Isolate::CreateParams create_params;
    create_params.array_buffer_allocator =
            v8::ArrayBuffer::Allocator::NewDefaultAllocator();
    v8::Isolate *isolate = v8::Isolate::New(create_params);

    // this ctor does not resolve
    auto script_origin = new v8::ScriptOrigin(isolate, v8::String::NewFromUtf8(isolate, "main.mjs"));
}


Solution

  • Trying to compile this snippet tells you exactly what's going on:

    $ clang++ -std=c++17 test.cc -o test.bin -Iv8/include
    test.cc:18:30: error: no matching constructor for initialization of 'v8::ScriptOrigin'
        auto script_origin = new v8::ScriptOrigin(isolate, v8::String::NewFromUtf8(isolate, "main.mjs"));
                                 ^                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    ./v8/include/v8-message.h:64:13: note: candidate constructor not viable: no known conversion from 'MaybeLocal<v8::String>' to 'Local<v8::Value>' for 2nd argument
      V8_INLINE ScriptOrigin(Isolate* isolate, Local<Value> resource_name,
                ^
    ./v8/include/v8-message.h:62:17: note: candidate constructor (the implicit copy constructor) not viable: requires 1 argument, but 2 were provided
    class V8_EXPORT ScriptOrigin {
                    ^
    ./v8/include/v8-message.h:62:17: note: candidate constructor (the implicit move constructor) not viable: requires 1 argument, but 2 were provided
    1 error generated.
    

    Note in particular the snippet
    no known conversion from 'MaybeLocal<v8::String>' to 'Local<v8::Value>' for 2nd argument.
    You're trying to call a constructor that doesn't exist. Your IDE and the compiler are correct in reporting this.

    Creating a new string can fail, in particular when the requested string is too large, so NewStringFromUtf8 returns a MaybeLocal, forcing calling code to check for errors and handle them appropriately. If the string is as short as "main.mjs", you can trust that allocating it won't fail, and simply use the .ToLocalChecked() conversion (which will crash your process if there was an error, so in general it's better to use the bool-returning .ToLocal(...) and handle errors gracefully).

    So if you change the last line to:

      auto script_origin = new v8::ScriptOrigin(
          isolate, 
          v8::String::NewFromUtf8(isolate, "main.mjs").ToLocalChecked());
    

    then this snippet will compile.

    Inlining has nothing to do with this, neither does the V8_INLINE macro, or the output of nm, or symbol export or other linker-related issues. It should go without saying that editing V8's header files is also not necessary for embedding V8.
    In general, when working with C++ it makes sense to distinguish clearly between compiler errors and linker errors. When the compiler is complaining, then looking into possible linking issues is "barking up the wrong tree".