blob: 8c382db57b70c82086fb62fd5430f29a94123c1d [file] [log] [blame]
// Copyright 2017 The Cobalt Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cobalt/script/v8c/v8c_global_environment.h"
#include <algorithm>
#include <utility>
#include "base/lazy_instance.h"
#include "base/strings/stringprintf.h"
#include "base/trace_event/trace_event.h"
#include "cobalt/base/polymorphic_downcast.h"
#include "cobalt/cache/cache.h"
#include "cobalt/script/javascript_engine.h"
#include "cobalt/script/v8c/embedded_resources.h"
#include "cobalt/script/v8c/entry_scope.h"
#include "cobalt/script/v8c/v8c_script_value_factory.h"
#include "cobalt/script/v8c/v8c_source_code.h"
#include "cobalt/script/v8c/v8c_user_object_holder.h"
#include "cobalt/script/v8c/v8c_value_handle.h"
#include "nb/memory_scope.h"
#include "starboard/common/murmurhash2.h"
namespace cobalt {
namespace script {
namespace v8c {
namespace {
std::string ExceptionToString(v8::Isolate* isolate,
const v8::TryCatch& try_catch) {
v8::HandleScope handle_scope(isolate);
v8::String::Utf8Value exception(isolate, try_catch.Exception());
v8::Local<v8::Message> message(try_catch.Message());
v8::Local<v8::Context> context = isolate->GetCurrentContext();
std::string string;
if (message.IsEmpty()) {
string.append(base::StringPrintf("%s\n", *exception));
} else {
v8::String::Utf8Value filename(isolate,
message->GetScriptOrigin().ResourceName());
int linenum;
linenum = message->GetLineNumber(context).To(&linenum) ? linenum : -1;
int colnum = message->GetStartColumn();
string.append(base::StringPrintf("%s:%i:%i %s\n", *filename, linenum,
colnum, *exception));
v8::String::Utf8Value sourceline(
isolate, message->GetSourceLine(context).ToLocalChecked());
string.append(base::StringPrintf("%s\n", *sourceline));
}
return string;
}
std::string ToStringOrNull(v8::Isolate* isolate, v8::Local<v8::Value> value) {
if (value.IsEmpty() || !value->IsString()) {
return "";
}
return *v8::String::Utf8Value(isolate, value.As<v8::String>());
}
uint32_t CreateJavaScriptCacheKey(const std::string& javascript_engine_version,
uint32_t cached_data_version_tag,
const std::string& source,
const std::string& origin) {
uint32_t res = starboard::MurmurHash2_32(javascript_engine_version.c_str(),
javascript_engine_version.size(),
cached_data_version_tag);
res = starboard::MurmurHash2_32(source.c_str(), source.size(), res);
res = starboard::MurmurHash2_32(origin.c_str(), origin.size(), res);
return res;
}
} // namespace
V8cGlobalEnvironment::V8cGlobalEnvironment(v8::Isolate* isolate)
: isolate_(isolate),
destruction_helper_(isolate),
wrapper_factory_(new WrapperFactory(isolate)),
script_value_factory_(new V8cScriptValueFactory(isolate)) {
TRACK_MEMORY_SCOPE("Javascript");
TRACE_EVENT0("cobalt::script",
"V8cGlobalEnvironment::V8cGlobalEnvironment()");
wrapper_factory_.reset(new WrapperFactory(isolate));
isolate_->SetData(kIsolateDataIndex, this);
DCHECK(isolate_->GetData(kIsolateDataIndex) == this);
isolate_->SetAllowCodeGenerationFromStringsCallback(
AllowCodeGenerationFromStringsCallback);
isolate_->SetAllowWasmCodeGenerationCallback(
[](v8::Local<v8::Context> context, v8::Local<v8::String> source) {
return false;
});
isolate_->AddMessageListenerWithErrorLevel(
MessageHandler,
v8::Isolate::kMessageError | v8::Isolate::kMessageWarning |
v8::Isolate::kMessageInfo | v8::Isolate::kMessageDebug |
v8::Isolate::kMessageLog);
}
V8cGlobalEnvironment::~V8cGlobalEnvironment() {
TRACE_EVENT0("cobalt::script",
"V8cGlobalEnvironment::~V8cGlobalEnvironment()");
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
InvalidateWeakPtrs();
}
void V8cGlobalEnvironment::CreateGlobalObject() {
TRACE_EVENT0("cobalt::script", "V8cGlobalEnvironment::CreateGlobalObject()");
TRACK_MEMORY_SCOPE("Javascript");
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
// Intentionally not an |EntryScope|, since the context doesn't exist yet.
v8::Isolate::Scope isolate_scope(isolate_);
v8::HandleScope handle_scope(isolate_);
v8::Local<v8::Context> context = v8::Context::New(isolate_);
context_.Reset(isolate_, context);
v8::Context::Scope context_scope(context);
EvaluateAutomatics();
}
bool V8cGlobalEnvironment::EvaluateScript(
const scoped_refptr<SourceCode>& source_code,
std::string* out_result_utf8) {
TRACK_MEMORY_SCOPE("Javascript");
TRACE_EVENT0("cobalt::script", "V8cGlobalEnvironment::EvaluateScript()");
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DCHECK(isolate_);
EntryScope entry_scope(isolate_);
v8::Local<v8::Context> context = isolate_->GetCurrentContext();
v8::TryCatch try_catch(isolate_);
v8::Local<v8::Value> result;
if (!EvaluateScriptInternal(source_code).ToLocal(&result)) {
if (!try_catch.HasCaught()) {
LOG(WARNING) << "Script evaluation failed with no JavaScript exception.";
return false;
}
// The MessageHandler appears to never get called under a |v8::TryCatch|
// block, even if we re-throw it. We work around this by manually passing
// it to the MessageHandler.
MessageHandler(try_catch.Message(), try_catch.Exception());
if (out_result_utf8) {
*out_result_utf8 = ExceptionToString(isolate_, try_catch);
}
return false;
}
if (out_result_utf8) {
V8cExceptionState exception_state(isolate_);
FromJSValue(isolate_, result, kNoConversionFlags, &exception_state,
out_result_utf8);
}
return true;
}
bool V8cGlobalEnvironment::EvaluateScript(
const scoped_refptr<SourceCode>& source_code,
const scoped_refptr<Wrappable>& owning_object,
base::Optional<ValueHandleHolder::Reference>* out_value_handle) {
TRACK_MEMORY_SCOPE("Javascript");
TRACE_EVENT0("cobalt::script", "V8cGlobalEnvironment::EvaluateScript()");
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
EntryScope entry_scope(isolate_);
v8::Local<v8::Context> context = isolate_->GetCurrentContext();
v8::TryCatch try_catch(isolate_);
v8::Local<v8::Value> result;
if (!EvaluateScriptInternal(source_code).ToLocal(&result)) {
if (!try_catch.HasCaught()) {
LOG(WARNING) << "Script evaluation failed with no JavaScript exception.";
}
// The MessageHandler appears to never get called under a |v8::TryCatch|
// block, even if we re-throw it. We work around this by manually passing
// it to the MessageHandler.
MessageHandler(try_catch.Message(), try_catch.Exception());
return false;
}
if (out_value_handle) {
V8cValueHandleHolder v8c_value_handle_holder(isolate_, result);
out_value_handle->emplace(owning_object.get(), v8c_value_handle_holder);
}
return true;
}
std::vector<StackFrame> V8cGlobalEnvironment::GetStackTrace(int max_frames) {
TRACE_EVENT0("cobalt::script", "V8cGlobalEnvironment::GetStackTrace()");
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
// cobalt::script treats |max_frames| being set to 0 as "the entire stack",
// while V8 interprets the frame count being set to 0 as "give me 0 frames",
// so we have to translate between the two.
const int kV8CallMaxFrameAmount = 4096;
int v8_max_frames = (max_frames == 0) ? kV8CallMaxFrameAmount : max_frames;
v8::HandleScope handle_scope(isolate_);
v8::Local<v8::StackTrace> stack_trace =
v8::StackTrace::CurrentStackTrace(isolate_, v8_max_frames);
std::vector<StackFrame> result;
for (int i = 0; i < stack_trace->GetFrameCount(); i++) {
v8::Local<v8::StackFrame> stack_frame = stack_trace->GetFrame(isolate_, i);
v8::String::Utf8Value function_name(isolate_,
stack_frame->GetFunctionName());
v8::String::Utf8Value script_name(isolate_, stack_frame->GetScriptName());
std::string function_name_str, script_name_str;
if (*function_name) {
function_name_str = std::string(*function_name);
}
if (*script_name) {
script_name_str = std::string(*script_name);
}
result.emplace_back(stack_frame->GetLineNumber(), stack_frame->GetColumn(),
function_name_str, script_name_str);
}
return result;
}
void V8cGlobalEnvironment::AddRoot(Traceable* traceable) {
V8cEngine::GetFromIsolate(isolate_)->heap_tracer()->AddRoot(traceable);
}
void V8cGlobalEnvironment::RemoveRoot(Traceable* traceable) {
V8cEngine::GetFromIsolate(isolate_)->heap_tracer()->RemoveRoot(traceable);
}
void V8cGlobalEnvironment::PreventGarbageCollection(
const scoped_refptr<Wrappable>& wrappable) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
AddRoot(wrappable.get());
}
void V8cGlobalEnvironment::AllowGarbageCollection(Wrappable* wrappable) {
TRACK_MEMORY_SCOPE("Javascript");
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
RemoveRoot(wrappable);
}
void V8cGlobalEnvironment::DisableEval(const std::string& message) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
EntryScope entry_scope(isolate_);
v8::Local<v8::Context> context = isolate_->GetCurrentContext();
context->AllowCodeGenerationFromStrings(false);
context->SetErrorMessageForCodeGenerationFromStrings(
v8::String::NewFromUtf8(isolate_, message.c_str(),
v8::NewStringType::kNormal)
.ToLocalChecked());
}
void V8cGlobalEnvironment::EnableEval() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
EntryScope entry_scope(isolate_);
v8::Local<v8::Context> context = isolate_->GetCurrentContext();
context->AllowCodeGenerationFromStrings(true);
}
void V8cGlobalEnvironment::DisableJit() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
LOG(INFO) << "V8 version " << V8_MAJOR_VERSION << '.' << V8_MINOR_VERSION
<< "can only be run with JIT enabled, ignoring |DisableJit| call.";
}
void V8cGlobalEnvironment::SetReportEvalCallback(
const base::Closure& report_eval) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
report_eval_ = report_eval;
}
void V8cGlobalEnvironment::SetReportErrorCallback(
const ReportErrorCallback& report_error_callback) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
report_error_callback_ = report_error_callback;
}
void V8cGlobalEnvironment::Bind(const std::string& identifier,
const scoped_refptr<Wrappable>& impl) {
TRACE_EVENT0("cobalt::script", "V8cGlobalEnvironment::Bind()");
TRACK_MEMORY_SCOPE("Javascript");
DCHECK(impl);
EntryScope entry_scope(isolate_);
v8::Local<v8::Context> context = isolate_->GetCurrentContext();
v8::Local<v8::Object> wrapper = wrapper_factory_->GetWrapper(impl);
v8::Local<v8::Object> global_object = context->Global();
v8::Maybe<bool> set_result = global_object->Set(
context,
v8::String::NewFromUtf8(isolate_, identifier.c_str(),
v8::NewStringType::kInternalized)
.ToLocalChecked(),
wrapper);
DCHECK(set_result.FromJust());
}
void V8cGlobalEnvironment::BindTo(const std::string& identifier,
const scoped_refptr<Wrappable>& impl,
const std::string& local_object_name) {
TRACE_EVENT0("cobalt::script", "V8cGlobalEnvironment::BindTo()");
TRACK_MEMORY_SCOPE("Javascript");
DCHECK(impl);
EntryScope entry_scope(isolate_);
v8::Local<v8::Context> context = isolate_->GetCurrentContext();
v8::Local<v8::Object> wrapper = wrapper_factory_->GetWrapper(impl);
v8::Local<v8::Object> global_object = context->Global();
v8::Local<v8::String> local_object_string(
v8::String::NewFromUtf8(isolate_, local_object_name.c_str(),
v8::NewStringType::kInternalized)
.ToLocalChecked());
v8::Local<v8::Object> local_object = v8::Local<v8::Object>::Cast(
global_object->Get(context, local_object_string).ToLocalChecked());
v8::Maybe<bool> set_result = local_object->Set(
context,
v8::String::NewFromUtf8(isolate_, identifier.c_str(),
v8::NewStringType::kInternalized)
.ToLocalChecked(),
wrapper);
DCHECK(set_result.FromJust());
}
ScriptValueFactory* V8cGlobalEnvironment::script_value_factory() {
DCHECK(script_value_factory_);
return script_value_factory_.get();
}
V8cGlobalEnvironment::DestructionHelper::~DestructionHelper() {
TRACE_EVENT0("cobalt::script",
"V8cGlobalEnvironment::DestructionHelper::~DestructionHelper()");
V8cEngine::GetFromIsolate(isolate_)->heap_tracer()->DisableForShutdown();
isolate_->SetData(kIsolateDataIndex, nullptr);
isolate_->LowMemoryNotification();
// Another GC to make sure global object is collected.
isolate_->LowMemoryNotification();
isolate_->SetEmbedderHeapTracer(nullptr);
}
// static
bool V8cGlobalEnvironment::AllowCodeGenerationFromStringsCallback(
v8::Local<v8::Context> context, v8::Local<v8::String> source) {
V8cGlobalEnvironment* global_environment =
V8cGlobalEnvironment::GetFromIsolate(context->GetIsolate());
DCHECK(global_environment);
if (!global_environment->report_eval_.is_null()) {
global_environment->report_eval_.Run();
}
// This callback should only be called while code generation from strings is
// not allowed from within V8, so this should always be false. Note that
// WebAssembly code generation will fall back to this callback if a
// WebAssembly callback has not been explicitly set, however we *have* set
// one.
DCHECK_EQ(context->IsCodeGenerationFromStringsAllowed(), false);
return context->IsCodeGenerationFromStringsAllowed();
}
// static
void V8cGlobalEnvironment::MessageHandler(v8::Local<v8::Message> message,
v8::Local<v8::Value> data) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
V8cGlobalEnvironment* global_environment =
V8cGlobalEnvironment::GetFromIsolate(isolate);
if (isolate->GetEnteredContext().IsEmpty()) {
return;
}
if (message->ErrorLevel() != v8::Isolate::kMessageError) {
return;
}
v8::Local<v8::Context> context = isolate->GetEnteredContext();
ErrorReport error_report;
error_report.message = *v8::String::Utf8Value(isolate, message->Get());
error_report.filename =
ToStringOrNull(isolate, message->GetScriptResourceName());
int line_number = 0;
int column_number = 0;
if (message->GetLineNumber(context).To(&line_number) &&
message->GetStartColumn(context).To(&column_number)) {
column_number++;
}
error_report.line_number = line_number;
error_report.column_number = column_number;
error_report.is_muted = message->IsSharedCrossOrigin();
error_report.error.reset(new V8cValueHandleHolder(isolate, data));
if (!global_environment->report_error_callback_.is_null()) {
global_environment->report_error_callback_.Run(error_report);
}
}
v8::MaybeLocal<v8::Value> V8cGlobalEnvironment::EvaluateScriptInternal(
const scoped_refptr<SourceCode>& source_code) {
TRACK_MEMORY_SCOPE("Javascript");
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
// Note that we expect an |EntryScope| and |v8::TryCatch| to have been set
// up by our caller.
v8::Local<v8::Script> script;
if (!CompileWithCaching(source_code).ToLocal(&script)) {
return {};
}
TRACE_EVENT0("cobalt::script", "v8::Script::Run()");
return script->Run(isolate_->GetCurrentContext());
}
v8::MaybeLocal<v8::Script> V8cGlobalEnvironment::CompileWithCaching(
const scoped_refptr<SourceCode>& source_code) {
TRACE_EVENT0("cobalt::script", "V8cGlobalEnvironment::CompileWithCaching()");
v8::Local<v8::Context> context = isolate_->GetCurrentContext();
V8cSourceCode* v8c_source_code =
base::polymorphic_downcast<V8cSourceCode*>(source_code.get());
const base::SourceLocation& source_location = v8c_source_code->location();
v8::Local<v8::String> source;
if (!v8::String::NewFromUtf8(isolate_, v8c_source_code->source_utf8().c_str(),
v8::NewStringType::kNormal,
v8c_source_code->source_utf8().length())
.ToLocal(&source)) {
LOG(WARNING) << "Failed to convert source code to V8 UTF-8 string.";
return {};
}
v8::Local<v8::String> resource_name;
if (!v8::String::NewFromUtf8(isolate_, source_location.file_path.c_str(),
v8::NewStringType::kNormal)
.ToLocal(&resource_name)) {
// Technically possible, but whoa man should this never happen.
LOG(WARNING) << "Failed to convert source location file path \""
<< source_location.file_path << "\" to a V8 UTF-8 string.";
return {};
}
// Note that |v8::ScriptOrigin| offsets are 0-based, whereas
// |SourceLocation| line/column numbers are 1-based, so subtract 1 to
// translate between the two.
v8::ScriptOrigin script_origin(
/*resource_name=*/resource_name,
/*resource_line_offset=*/
v8::Integer::New(isolate_, source_location.line_number - 1),
/*resource_column_offset=*/
v8::Integer::New(isolate_, source_location.column_number - 1),
/*resource_is_shared_cross_origin=*/
v8::Boolean::New(isolate_, v8c_source_code->is_muted()));
std::string javascript_engine_version =
script::GetJavaScriptEngineNameAndVersion();
uint32_t javascript_cache_key = CreateJavaScriptCacheKey(
javascript_engine_version, v8::ScriptCompiler::CachedDataVersionTag(),
v8c_source_code->source_utf8(), source_location.file_path);
auto retrieved_cached_data = cobalt::cache::Cache::GetInstance()->Retrieve(
disk_cache::ResourceType::kCompiledScript, javascript_cache_key,
[&]() -> std::unique_ptr<std::vector<uint8_t>> {
v8::Local<v8::Script> script;
{
TRACE_EVENT0("cobalt::script", "v8::Script::Compile()");
if (!v8::Script::Compile(context, source, &script_origin)
.ToLocal(&script)) {
return nullptr;
}
}
std::unique_ptr<v8::ScriptCompiler::CachedData> cached_data(
v8::ScriptCompiler::CreateCodeCache(script->GetUnboundScript()));
return std::make_unique<std::vector<uint8_t>>(
cached_data->data, cached_data->data + cached_data->length);
});
if (!retrieved_cached_data) {
return {};
}
v8::ScriptCompiler::CachedData* cached_code =
new v8::ScriptCompiler::CachedData(
retrieved_cached_data->data(), retrieved_cached_data->size(),
v8::ScriptCompiler::CachedData::BufferNotOwned);
// The script_source owns the cached_code object.
v8::ScriptCompiler::Source script_source(source, script_origin, cached_code);
{
TRACE_EVENT0("cobalt::script", "v8::Script::Compile()");
v8::Local<v8::Script> script;
if (v8::ScriptCompiler::Compile(context, &script_source,
v8::ScriptCompiler::kConsumeCodeCache)
.ToLocal(&script) &&
!cached_code->rejected) {
return script;
}
}
cobalt::cache::Cache::GetInstance()->Delete(
disk_cache::ResourceType::kCompiledScript, javascript_cache_key);
LOG(WARNING)
<< "CompileWithCaching: Failed to reuse the cached script rejected="
<< cached_code->rejected << ", key=" << javascript_cache_key;
return {};
}
void V8cGlobalEnvironment::EvaluateEmbeddedScript(const unsigned char* data,
size_t size,
const char* filename) {
TRACE_EVENT1("cobalt::script", "V8cGlobalEnvironment::EvaluateEmbeddedScript",
"filename", filename);
TRACK_MEMORY_SCOPE("Javascript");
std::string source(reinterpret_cast<const char*>(data), size);
scoped_refptr<SourceCode> source_code =
new V8cSourceCode(source, base::SourceLocation(filename, 1, 1));
std::string result;
bool success = EvaluateScript(source_code, &result);
if (!success) {
DLOG(FATAL) << result;
}
}
void V8cGlobalEnvironment::EvaluateAutomatics() {
TRACE_EVENT0("cobalt::script", "V8cGlobalEnvironment::EvaluateAutomatics()");
EvaluateEmbeddedScript(
V8cEmbeddedResources::byte_length_queuing_strategy_js,
sizeof(V8cEmbeddedResources::byte_length_queuing_strategy_js),
"byte_length_queuing_strategy.js");
EvaluateEmbeddedScript(
V8cEmbeddedResources::count_queuing_strategy_js,
sizeof(V8cEmbeddedResources::count_queuing_strategy_js),
"count_queuing_strategy.js");
EvaluateEmbeddedScript(V8cEmbeddedResources::readable_stream_js,
sizeof(V8cEmbeddedResources::readable_stream_js),
"readable_stream.js");
EvaluateEmbeddedScript(V8cEmbeddedResources::fetch_js,
sizeof(V8cEmbeddedResources::fetch_js), "fetch.js");
}
bool V8cGlobalEnvironment::HasInterfaceData(int key) const {
DCHECK_GE(key, 0);
if (key >= cached_interface_data_.size()) {
return false;
}
return !cached_interface_data_[key].IsEmpty();
}
v8::Local<v8::FunctionTemplate> V8cGlobalEnvironment::GetInterfaceData(
int key) const {
DCHECK(HasInterfaceData(key));
return cached_interface_data_[key].Get(isolate_);
}
void V8cGlobalEnvironment::AddInterfaceData(
int key, v8::Local<v8::FunctionTemplate> function_template) {
DCHECK(!HasInterfaceData(key));
if (key >= cached_interface_data_.size()) {
cached_interface_data_.resize(key + 1);
}
DCHECK(!HasInterfaceData(key));
cached_interface_data_[key].Set(isolate_, function_template);
}
} // namespace v8c
} // namespace script
} // namespace cobalt