mirror of
https://github.com/electron/electron.git
synced 2026-10-28 19:54:46 +08:00
* refactor: add a native peer helper for cppgc wrappers cppgc wrapper destructors run during sweeping, where they must not touch other GC objects, run JavaScript, or tear down native registrations. Debugger, Notification and WebContents each hand rolled an off-heap peer for this. Add NativePeer<Wrapper>, which owns native registrations, resources, and independent V8 roots for a wrapper. The wrapper's deleter only queues the peer. Queued peers are released and deleted from a posted task. At shutdown they are released while V8 can still run and deleted after the isolate is disposed, so peer teardown never runs inside a cppgc finalizer and nothing leaks. * chore: apply review feedback Co-authored-by: Samuel Attard <sam@electronjs.org> * chore: apply review feedback Co-authored-by: Samuel Attard <sam@electronjs.org> --------- Co-authored-by: Samuel Attard <sam@electronjs.org>
346 lines
13 KiB
C++
346 lines
13 KiB
C++
// Copyright (c) 2013 GitHub, Inc.
|
|
// Use of this source code is governed by the MIT license that can be
|
|
// found in the LICENSE file.
|
|
|
|
#include "shell/browser/javascript_environment.h"
|
|
|
|
#include <memory>
|
|
#include <string>
|
|
#include <utility>
|
|
|
|
#include "base/check.h"
|
|
#include "base/command_line.h"
|
|
#include "base/compiler_specific.h"
|
|
#include "base/containers/span.h"
|
|
#include "base/logging.h"
|
|
#include "base/synchronization/lock.h"
|
|
#include "base/task/current_thread.h"
|
|
#include "base/task/single_thread_task_runner.h"
|
|
#include "base/task/thread_pool/initialization_util.h"
|
|
#include "base/thread_annotations.h"
|
|
#include "electron/snapshot_checksum.h"
|
|
#include "gin/array_buffer.h"
|
|
#include "gin/per_isolate_data.h"
|
|
#include "gin/public/isolate_holder.h"
|
|
#include "gin/v8_initializer.h"
|
|
#include "partition_alloc/partition_alloc_constants.h"
|
|
#include "shell/browser/microtasks_runner.h"
|
|
#include "shell/browser/native_peer.h"
|
|
#include "shell/common/gin_helper/cleaned_up_at_exit.h"
|
|
#include "shell/common/node_includes.h"
|
|
#include "shell/common/node_util.h"
|
|
#include "shell/common/process_util.h"
|
|
#include "third_party/abseil-cpp/absl/container/flat_hash_map.h"
|
|
#include "third_party/blink/public/common/switches.h"
|
|
#include "third_party/electron_node/src/node_snapshot_builder.h"
|
|
#include "third_party/electron_node/src/node_wasm_web_api.h"
|
|
#include "v8/include/v8-initialization.h"
|
|
#include "v8/include/v8-isolate.h"
|
|
#include "v8/include/v8-locker.h"
|
|
#include "v8/include/v8-platform.h"
|
|
#include "v8/include/v8-snapshot.h"
|
|
|
|
namespace {
|
|
v8::Isolate* g_isolate;
|
|
}
|
|
|
|
namespace electron {
|
|
|
|
namespace {
|
|
|
|
#if defined(V8_ENABLE_SANDBOX)
|
|
// V8's built-in in-sandbox allocator (behind ArrayBuffer::Allocator::
|
|
// NewDefaultAllocator(), which Node uses for Buffers) is a slow fallback that
|
|
// embedders are expected to replace; route it to gin's PartitionAlloc
|
|
// partition inside the sandbox, as Blink does for renderers. Requests beyond
|
|
// PartitionAlloc's single-allocation cap (~2 GiB) come straight from the
|
|
// sandbox's address space instead.
|
|
class InSandboxAllocator final : public v8::Allocator {
|
|
public:
|
|
void* Allocate(size_t size) override {
|
|
if (size > partition_alloc::MaxAllocationSize())
|
|
return AllocatePages(size);
|
|
return gin::ArrayBufferAllocator::SharedInstance()->Allocate(size);
|
|
}
|
|
void* AllocateUninitialized(size_t size) override {
|
|
if (size > partition_alloc::MaxAllocationSize())
|
|
return AllocatePages(size);
|
|
return gin::ArrayBufferAllocator::SharedInstance()->AllocateUninitialized(
|
|
size);
|
|
}
|
|
void* AllocateUninitializedOrCrash(size_t size) override {
|
|
void* result = AllocateUninitialized(size);
|
|
CHECK(result);
|
|
return result;
|
|
}
|
|
void Free(void* ptr) override {
|
|
if (!ptr)
|
|
return;
|
|
{
|
|
base::AutoLock lock(lock_);
|
|
auto it = pages_.find(ptr);
|
|
if (it != pages_.end()) {
|
|
v8::V8::GetSandboxAddressSpace()->FreePages(
|
|
reinterpret_cast<uintptr_t>(ptr), it->second);
|
|
pages_.erase(it);
|
|
return;
|
|
}
|
|
}
|
|
gin::ArrayBufferAllocator::SharedInstance()->Free(ptr, 0);
|
|
}
|
|
|
|
private:
|
|
// Fresh sandbox pages are zero-filled, so this serves both entry points.
|
|
void* AllocatePages(size_t size) {
|
|
v8::VirtualAddressSpace* space = v8::V8::GetSandboxAddressSpace();
|
|
size_t granularity = space->allocation_granularity();
|
|
size_t rounded = (size + granularity - 1) / granularity * granularity;
|
|
if (rounded < size)
|
|
return nullptr;
|
|
uintptr_t address =
|
|
space->AllocatePages(v8::VirtualAddressSpace::kNoHint, rounded,
|
|
granularity, v8::PagePermissions::kReadWrite);
|
|
if (!address)
|
|
return nullptr;
|
|
base::AutoLock lock(lock_);
|
|
pages_.emplace(reinterpret_cast<void*>(address), rounded);
|
|
return reinterpret_cast<void*>(address);
|
|
}
|
|
|
|
base::Lock lock_;
|
|
absl::flat_hash_map<void*, size_t> pages_ GUARDED_BY(lock_);
|
|
};
|
|
#endif
|
|
|
|
// Whether the V8 snapshot blob this process loaded (v8_context_snapshot.bin,
|
|
// or in the browser process under the LoadBrowserProcessSpecificV8Snapshot
|
|
// fuse, browser_v8_context_snapshot.bin) is the one this build shipped, as
|
|
// opposed to a custom blob produced with electron-mksnapshot. Compares size
|
|
// and V8's blob header -- which embeds V8's checksum of the rest of the
|
|
// blob -- against build-time constants, so no hashing at startup and no
|
|
// per-process-type knowledge of which file was picked.
|
|
bool LoadedV8SnapshotIsBuiltIn() {
|
|
v8::StartupData blob{nullptr, 0};
|
|
gin::V8Initializer::GetV8ExternalSnapshotData(&blob);
|
|
if (!blob.data || blob.raw_size <= 0)
|
|
return false;
|
|
const auto loaded = base::as_bytes(UNSAFE_BUFFERS(
|
|
base::span<const char>(blob.data, static_cast<size_t>(blob.raw_size))));
|
|
const auto expected_prefix = base::span(snapshot_checksum::kHeaderPrefix);
|
|
return loaded.size() == snapshot_checksum::kSize &&
|
|
loaded.size() >= expected_prefix.size() &&
|
|
loaded.first(expected_prefix.size()) == expected_prefix;
|
|
}
|
|
|
|
// The Node startup snapshot a JavascriptEnvironment's isolate is created
|
|
// from, when one is embedded and this process is allowed to consume it: the
|
|
// browser process, ELECTRON_RUN_AS_NODE children (shell/app/node_main.cc,
|
|
// which also run without a --type) and the utility process hosting the node
|
|
// service (the only utility-process code that creates a JavascriptEnvironment).
|
|
// Each creates exactly one main isolate -- Node worker threads inherit the
|
|
// snapshot through IsolateData -- so V8's one-read-only-heap-per-process rule
|
|
// holds even though the process-wide external startup data content/gin loaded
|
|
// is the v8 context snapshot. (Renderers never have a JavascriptEnvironment;
|
|
// their isolates belong to blink.)
|
|
const node::SnapshotData* NodeSnapshotForThisProcess() {
|
|
static const node::SnapshotData* const snapshot =
|
|
[]() -> const node::SnapshotData* {
|
|
if (!electron::IsBrowserProcess() && !electron::IsUtilityProcess())
|
|
return nullptr;
|
|
const node::SnapshotData* embedded =
|
|
node::SnapshotBuilder::GetEmbeddedSnapshotData();
|
|
if (!embedded)
|
|
return nullptr;
|
|
// A custom V8 snapshot (electron-mksnapshot, or the browser-specific blob
|
|
// the LoadBrowserProcessSpecificV8Snapshot fuse selects) exists to put
|
|
// objects into the main process's context. The embedded Node snapshot was
|
|
// built on top of the stock blob at build time and cannot contain them, so
|
|
// creating the main context from it would silently drop the custom
|
|
// snapshot's contents. Fall back to bootstrapping Node from scratch on the
|
|
// loaded blob, as builds without a Node snapshot do.
|
|
if (!LoadedV8SnapshotIsBuiltIn()) {
|
|
VLOG(1) << "Custom V8 snapshot loaded; not creating this process's "
|
|
"Node.js environment from the embedded Node snapshot.";
|
|
return nullptr;
|
|
}
|
|
return embedded;
|
|
}();
|
|
return snapshot;
|
|
}
|
|
|
|
std::unique_ptr<gin::IsolateHolder> CreateIsolateHolder(
|
|
v8::Isolate* isolate,
|
|
size_t* max_young_generation_size) {
|
|
std::unique_ptr<v8::Isolate::CreateParams> create_params =
|
|
gin::IsolateHolder::getDefaultIsolateParams();
|
|
// The value is needed to adjust heap limit when capturing
|
|
// snapshot via v8.setHeapSnapshotNearHeapLimit(limit) or
|
|
// --heapsnapshot-near-heap-limit=max_count.
|
|
*max_young_generation_size =
|
|
create_params->constraints.max_young_generation_size_in_bytes();
|
|
// Electron: create the browser-process isolate from the embedded Node
|
|
// startup snapshot when one is present, so the Node bootstrap is
|
|
// deserialized instead of recompiled at app start.
|
|
if (const node::SnapshotData* sd = NodeSnapshotForThisProcess()) {
|
|
node::SnapshotBuilder::InitializeIsolateParams(sd, create_params.get());
|
|
}
|
|
// Align behavior with V8 Isolate default for Node.js.
|
|
// This is necessary for important aspects of Node.js
|
|
// including heap and cpu profilers to function properly.
|
|
|
|
return std::make_unique<gin::IsolateHolder>(
|
|
base::SingleThreadTaskRunner::GetCurrentDefault(),
|
|
gin::IsolateHolder::kSingleThread,
|
|
gin::IsolateHolder::IsolateType::kUtility, std::move(create_params),
|
|
gin::IsolateHolder::IsolateCreationMode::kNormal, nullptr, nullptr,
|
|
isolate);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
JavascriptEnvironment::JavascriptEnvironment(
|
|
uv_loop_t* event_loop,
|
|
bool setup_wasm_streaming,
|
|
v8::TracingController* tracing_controller)
|
|
: isolate_holder_{CreateIsolateHolder(
|
|
Initialize(event_loop, setup_wasm_streaming, tracing_controller),
|
|
&max_young_generation_size_)},
|
|
locker_{std::in_place, isolate()} {
|
|
v8::Isolate* const isolate = this->isolate();
|
|
isolate->Enter();
|
|
|
|
// Every JavascriptEnvironment hosts a Node.js environment (browser process,
|
|
// ELECTRON_RUN_AS_NODE, the utility-process node service). Install the
|
|
// build-time builtin code cache before node::NewContext below constructs
|
|
// the first BuiltinLoader (for the per-context scripts).
|
|
electron::util::InstallProcessCodeCache();
|
|
|
|
// Electron: when consuming the embedded Node startup snapshot, the main
|
|
// context is materialized from the snapshot inside node::CreateEnvironment
|
|
// (Context::FromSnapshot) and entered in electron_browser_main_parts.cc
|
|
// after that. Creating a fresh node::NewContext here would be wasted work.
|
|
if (NodeSnapshotForThisProcess() != nullptr)
|
|
return;
|
|
|
|
v8::HandleScope scope{isolate};
|
|
auto context = node::NewContext(isolate);
|
|
CHECK(!context.IsEmpty());
|
|
|
|
context->Enter();
|
|
}
|
|
|
|
JavascriptEnvironment::~JavascriptEnvironment() {
|
|
DCHECK_NE(platform_, nullptr);
|
|
v8::Isolate* isolate = this->isolate();
|
|
|
|
// PostMainMessageLoopRun() is skipped when startup fails early (e.g. no
|
|
// display), so dispose the runner here while the isolate is still alive.
|
|
if (microtasks_runner_)
|
|
DestroyMicrotasksRunner();
|
|
|
|
{
|
|
v8::HandleScope scope{isolate};
|
|
isolate->GetCurrentContext()->Exit();
|
|
}
|
|
isolate->Exit();
|
|
g_isolate = nullptr;
|
|
|
|
// Deinit gin::IsolateHolder prior to calling NodePlatform::UnregisterIsolate.
|
|
// Otherwise cppgc::internal::Sweeper::Start will try to request a task runner
|
|
// from the NodePlatform with an already unregistered isolate.
|
|
locker_.reset();
|
|
isolate_holder_.reset();
|
|
|
|
platform_->UnregisterIsolate(isolate);
|
|
|
|
// Heap teardown destroys the remaining wrappers, which queue their released
|
|
// peers. Delete those peers now that no cppgc finalizer can be running.
|
|
NativePeerBase::DeleteQueuedPeersAfterIsolateDisposal();
|
|
}
|
|
|
|
v8::Isolate* JavascriptEnvironment::Initialize(
|
|
uv_loop_t* event_loop,
|
|
bool setup_wasm_streaming,
|
|
v8::TracingController* tracing_controller) {
|
|
auto* cmd = base::CommandLine::ForCurrentProcess();
|
|
// --js-flags.
|
|
std::string js_flags = "--no-freeze-flags-after-init ";
|
|
js_flags.append(cmd->GetSwitchValueASCII(blink::switches::kJavaScriptFlags));
|
|
v8::V8::SetFlagsFromString(js_flags.c_str(), js_flags.size());
|
|
|
|
// The V8Platform of gin relies on Chromium's task schedule, which has not
|
|
// been started at this point, so we have to rely on Node's V8Platform.
|
|
platform_ = node::MultiIsolatePlatform::Create(
|
|
base::RecommendedMaxNumberOfThreadsInThreadGroup(3, 8, 0.1, 0),
|
|
tracing_controller, gin::V8Platform::Get()->GetPageAllocator());
|
|
|
|
v8::V8::InitializePlatform(platform_.get());
|
|
gin::IsolateHolder::Initialize(
|
|
gin::IsolateHolder::kNonStrictMode,
|
|
gin::ArrayBufferAllocator::SharedInstance(),
|
|
nullptr /* external_reference_table */, js_flags,
|
|
false /* disallow_v8_feature_flag_overrides */,
|
|
nullptr /* fatal_error_callback */, nullptr /* oom_error_callback */,
|
|
false /* create_v8_platform */);
|
|
|
|
#if defined(V8_ENABLE_SANDBOX)
|
|
v8::V8::SetInSandboxAllocator(std::make_shared<InSandboxAllocator>());
|
|
#endif
|
|
|
|
v8::Isolate* isolate = v8::Isolate::Allocate();
|
|
platform_->RegisterIsolate(isolate, event_loop);
|
|
|
|
// This is done here because V8 checks for the callback in NewContext.
|
|
// Our setup order doesn't allow for calling SetupIsolateForNode
|
|
// before NewContext without polluting JavaScriptEnvironment with
|
|
// Node.js logic and so we conditionally do it here to keep
|
|
// concerns separate.
|
|
if (setup_wasm_streaming) {
|
|
isolate->SetWasmStreamingCallback(
|
|
node::wasm_web_api::StartStreamingCompilation);
|
|
}
|
|
|
|
g_isolate = isolate;
|
|
|
|
return isolate;
|
|
}
|
|
|
|
v8::Isolate* JavascriptEnvironment::isolate() const {
|
|
return isolate_holder_->isolate();
|
|
}
|
|
|
|
// static
|
|
v8::Isolate* JavascriptEnvironment::GetIsolate() {
|
|
CHECK(g_isolate);
|
|
return g_isolate;
|
|
}
|
|
|
|
// static
|
|
const node::SnapshotData* JavascriptEnvironment::NodeSnapshot() {
|
|
return NodeSnapshotForThisProcess();
|
|
}
|
|
|
|
void JavascriptEnvironment::CreateMicrotasksRunner() {
|
|
DCHECK(!microtasks_runner_);
|
|
microtasks_runner_ = std::make_unique<MicrotasksRunner>(isolate());
|
|
base::CurrentThread::Get()->AddTaskObserver(microtasks_runner_.get());
|
|
}
|
|
|
|
void JavascriptEnvironment::DestroyMicrotasksRunner() {
|
|
DCHECK(microtasks_runner_);
|
|
{
|
|
v8::HandleScope scope{isolate()};
|
|
gin_helper::CleanedUpAtExit::DoCleanup();
|
|
// After DoCleanup() so that observers created by JS that ran during it
|
|
// (e.g. a webContents 'destroyed' handler) are notified too.
|
|
microtasks_runner_->NotifyBeforeDispose();
|
|
// Posted peer release tasks will not run after this point. Release the
|
|
// peers already queued while V8 can still run.
|
|
NativePeerBase::ReleaseQueuedPeersForShutdown();
|
|
}
|
|
base::CurrentThread::Get()->RemoveTaskObserver(microtasks_runner_.get());
|
|
microtasks_runner_.reset();
|
|
}
|
|
|
|
} // namespace electron
|