Skip to content

Commit

Permalink
feat: Implement Rust-side const ExternalOneByteStringResource subclass (
Browse files Browse the repository at this point in the history
denoland#1275)

MSVC and Itanium C++ ABIs agree that for simple inheritance the basic structure of a vtable contains metadata fields at a "negative offset" from the vtable pointer, and at zero or positive offsets come the virtual function pointers in the order of declaration. The only difference between the two is that MSVC only places the virtual deleting destructor in the vtable while Itanium ABI places both the deleting and the complete object destructors in it, leading to a vtable that is one pointer larger in Itanium / on Linux. Also MSVC only has a single metadata field instead of two for Itanium. Itanium inlines the base offset into the vtable while MSVC keeps it in what is essentially the entry point into the type info data.

Since the two are so similar, creating a custom vtable on Rust-side is pretty easy and can be done entirely at compile-time, meaning that instances of the class can also be created entirely at compile time. This leads to fully const external strings being possible.
  • Loading branch information
aapoalas committed Jul 12, 2023
1 parent 4dd8b60 commit 308a113
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 35 deletions.
92 changes: 57 additions & 35 deletions src/binding.cc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright 2019-2021 the Deno authors. All rights reserved. MIT license.
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <iostream>
Expand Down Expand Up @@ -850,7 +851,8 @@ two_pointers_t v8__ArrayBuffer__GetBackingStore(const v8::ArrayBuffer& self) {
return make_pod<two_pointers_t>(ptr_to_local(&self)->GetBackingStore());
}

bool v8__BackingStore__IsResizableByUserJavaScript(const v8::BackingStore& self) {
bool v8__BackingStore__IsResizableByUserJavaScript(
const v8::BackingStore& self) {
return ptr_to_local(&self)->IsResizableByUserJavaScript();
}

Expand Down Expand Up @@ -1013,6 +1015,33 @@ class ExternalStaticOneByteStringResource
const int _length;
};

// NOTE: This class is never used and only serves as a reference for
// the OneByteConst struct created on Rust-side.
class ExternalConstOneByteStringResource
: public v8::String::ExternalOneByteStringResource {
public:
ExternalConstOneByteStringResource(int length)
: _length(length) {
static_assert(offsetof(ExternalConstOneByteStringResource, _length) == 16,
"ExternalConstOneByteStringResource's length was not at offset 16");
static_assert(sizeof(ExternalConstOneByteStringResource) == 24,
"ExternalConstOneByteStringResource size was not 24");
static_assert(alignof(ExternalConstOneByteStringResource) == 8,
"ExternalConstOneByteStringResource align was not 8");
}
const char* data() const override { return nullptr; }
size_t length() const override { return _length; }
void Dispose() override {}

private:
const int _length;
};

const v8::String* v8__String__NewExternalOneByte(
v8::Isolate* isolate, v8::String::ExternalOneByteStringResource* resource) {
return maybe_local_to_ptr(v8::String::NewExternalOneByte(isolate, resource));
}

const v8::String* v8__String__NewExternalOneByteStatic(v8::Isolate* isolate,
const char* data,
int length) {
Expand Down Expand Up @@ -1150,8 +1179,7 @@ void v8__ObjectTemplate__SetNamedPropertyHandler(
v8::GenericNamedPropertyEnumeratorCallback enumerator,
v8::GenericNamedPropertyDefinerCallback definer,
v8::GenericNamedPropertyDescriptorCallback descriptor,
const v8::Value* data_or_null,
v8::PropertyHandlerFlags flags) {
const v8::Value* data_or_null, v8::PropertyHandlerFlags flags) {
ptr_to_local(&self)->SetHandler(v8::NamedPropertyHandlerConfiguration(
getter, setter, query, deleter, enumerator, definer, descriptor,
ptr_to_local(data_or_null), flags));
Expand All @@ -1165,8 +1193,7 @@ void v8__ObjectTemplate__SetIndexedPropertyHandler(
v8::IndexedPropertyEnumeratorCallback enumerator,
v8::IndexedPropertyDefinerCallback definer,
v8::IndexedPropertyDescriptorCallback descriptor,
const v8::Value* data_or_null,
v8::PropertyHandlerFlags flags) {
const v8::Value* data_or_null, v8::PropertyHandlerFlags flags) {
ptr_to_local(&self)->SetHandler(v8::IndexedPropertyHandlerConfiguration(
getter, setter, query, deleter, enumerator, definer, descriptor,
ptr_to_local(data_or_null), flags));
Expand Down Expand Up @@ -1269,9 +1296,9 @@ MaybeBool v8__Object__DefineOwnProperty(const v8::Object& self,
}

MaybeBool v8__Object__DefineProperty(const v8::Object& self,
const v8::Context& context,
const v8::Name& key,
v8::PropertyDescriptor& desc) {
const v8::Context& context,
const v8::Name& key,
v8::PropertyDescriptor& desc) {
return maybe_to_maybe_bool(ptr_to_local(&self)->DefineProperty(
ptr_to_local(&context), ptr_to_local(&key), desc));
}
Expand All @@ -1285,7 +1312,7 @@ MaybeBool v8__Object__SetAccessor(const v8::Object& self,
v8::PropertyAttribute attr) {
return maybe_to_maybe_bool(ptr_to_local(&self)->SetAccessor(
ptr_to_local(&context), ptr_to_local(&key), getter, setter,
ptr_to_local(data_or_null), v8::AccessControl::DEFAULT,attr));
ptr_to_local(data_or_null), v8::AccessControl::DEFAULT, attr));
}

v8::Isolate* v8__Object__GetIsolate(const v8::Object& self) {
Expand Down Expand Up @@ -1430,16 +1457,16 @@ MaybeBool v8__Object__HasPrivate(const v8::Object& self,
ptr_to_local(&context), ptr_to_local(&key)));
}

void v8__Object__GetPropertyAttributes(
const v8::Object& self, const v8::Context& context,
const v8::Value& key, v8::Maybe<v8::PropertyAttribute>* out) {
void v8__Object__GetPropertyAttributes(const v8::Object& self,
const v8::Context& context,
const v8::Value& key,
v8::Maybe<v8::PropertyAttribute>* out) {
*out = ptr_to_local(&self)->GetPropertyAttributes(ptr_to_local(&context),
ptr_to_local(&key));
}

const v8::Value* v8__Object__GetOwnPropertyDescriptor(
const v8::Object& self, const v8::Context& context,
const v8::Name& key) {
const v8::Object& self, const v8::Context& context, const v8::Name& key) {
return maybe_local_to_ptr(ptr_to_local(&self)->GetOwnPropertyDescriptor(
ptr_to_local(&context), ptr_to_local(&key)));
}
Expand All @@ -1450,7 +1477,6 @@ const v8::Array* v8__Object__PreviewEntries(
return maybe_local_to_ptr(ptr_to_local(&self)->PreviewEntries(is_key_value));
}


const v8::Array* v8__Array__New(v8::Isolate* isolate, int length) {
return local_to_ptr(v8::Array::New(isolate, length));
}
Expand Down Expand Up @@ -1536,7 +1562,7 @@ void v8__Set__Clear(const v8::Set& self) {
}

v8::Set* v8__Set__Add(const v8::Set& self, const v8::Context& context,
const v8::Value& key) {
const v8::Value& key) {
return maybe_local_to_ptr(
ptr_to_local(&self)->Add(ptr_to_local(&context), ptr_to_local(&key)));
}
Expand Down Expand Up @@ -1787,8 +1813,7 @@ const v8::Value* v8__Context__GetSecurityToken(const v8::Context& self) {
return local_to_ptr(value);
}

void v8__Context__SetSecurityToken(v8::Context& self,
const v8::Value* token) {
void v8__Context__SetSecurityToken(v8::Context& self, const v8::Value* token) {
auto c = ptr_to_local(&self);
c->SetSecurityToken(ptr_to_local(token));
}
Expand All @@ -1802,7 +1827,7 @@ void v8__Context__AllowCodeGenerationFromStrings(v8::Context& self, bool allow)
}

bool v8__Context_IsCodeGenerationFromStringsAllowed(v8::Context& self) {
return ptr_to_local(&self)->IsCodeGenerationFromStringsAllowed();
return ptr_to_local(&self)->IsCodeGenerationFromStringsAllowed();
}

const v8::Context* v8__Context__FromSnapshot(v8::Isolate* isolate,
Expand Down Expand Up @@ -1962,9 +1987,10 @@ int v8__Function__ScriptId(const v8::Function& self) {
return ptr_to_local(&self)->ScriptId();
}

const v8::ScriptOrigin* v8__Function__GetScriptOrigin(const v8::Function& self) {
std::unique_ptr<v8::ScriptOrigin> u =
std::make_unique<v8::ScriptOrigin>(ptr_to_local(&self)->GetScriptOrigin());
const v8::ScriptOrigin* v8__Function__GetScriptOrigin(
const v8::Function& self) {
std::unique_ptr<v8::ScriptOrigin> u = std::make_unique<v8::ScriptOrigin>(
ptr_to_local(&self)->GetScriptOrigin());
return u.release();
}

Expand Down Expand Up @@ -1993,10 +2019,9 @@ v8::CTypeInfo* v8__CTypeInfo__New__From__Slice(unsigned int len,
return v;
}

v8::CFunctionInfo* v8__CFunctionInfo__New(const v8::CTypeInfo& return_info,
unsigned int args_len,
v8::CTypeInfo* args_info,
v8::CFunctionInfo::Int64Representation repr) {
v8::CFunctionInfo* v8__CFunctionInfo__New(
const v8::CTypeInfo& return_info, unsigned int args_len,
v8::CTypeInfo* args_info, v8::CFunctionInfo::Int64Representation repr) {
std::unique_ptr<v8::CFunctionInfo> info = std::make_unique<v8::CFunctionInfo>(
v8::CFunctionInfo(return_info, args_len, args_info, repr));
return info.release();
Expand Down Expand Up @@ -2120,7 +2145,7 @@ const v8::StackTrace* v8__StackTrace__CurrentStackTrace(v8::Isolate* isolate,
}

const v8::String* v8__StackTrace__CurrentScriptNameOrSourceURL(
v8::Isolate* isolate) {
v8::Isolate* isolate) {
return local_to_ptr(v8::StackTrace::CurrentScriptNameOrSourceURL(isolate));
}

Expand Down Expand Up @@ -2300,13 +2325,11 @@ int v8__ScriptOrigin__ScriptId(const v8::ScriptOrigin& self) {
return ptr_to_local(&self)->ScriptId();
}

const v8::Value* v8__ScriptOrigin__ResourceName(
const v8::ScriptOrigin& self) {
const v8::Value* v8__ScriptOrigin__ResourceName(const v8::ScriptOrigin& self) {
return local_to_ptr(ptr_to_local(&self)->ResourceName());
}

const v8::Value* v8__ScriptOrigin__SourceMapUrl(
const v8::ScriptOrigin& self) {
const v8::Value* v8__ScriptOrigin__SourceMapUrl(const v8::ScriptOrigin& self) {
return local_to_ptr(ptr_to_local(&self)->SourceMapUrl());
}

Expand Down Expand Up @@ -3429,8 +3452,7 @@ bool v8__PropertyDescriptor__has_enumerable(
return self->has_enumerable();
}

bool v8__PropertyDescriptor__has_writable(
const v8::PropertyDescriptor* self) {
bool v8__PropertyDescriptor__has_writable(const v8::PropertyDescriptor* self) {
return self->has_writable();
}

Expand All @@ -3447,12 +3469,12 @@ bool v8__PropertyDescriptor__has_set(const v8::PropertyDescriptor* self) {
}

void v8__PropertyDescriptor__set_enumerable(v8::PropertyDescriptor* self,
bool enumurable) {
bool enumurable) {
self->set_enumerable(enumurable);
}

void v8__PropertyDescriptor__set_configurable(v8::PropertyDescriptor* self,
bool configurable) {
bool configurable) {
self->set_configurable(configurable);
}

Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ pub use script_compiler::CachedData;
pub use snapshot::FunctionCodeHandling;
pub use snapshot::StartupData;
pub use string::NewStringType;
pub use string::OneByteConst;
pub use string::WriteOptions;
pub use support::SharedPtr;
pub use support::SharedRef;
Expand Down
129 changes: 129 additions & 0 deletions src/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ extern "C" {
options: WriteOptions,
) -> int;

fn v8__String__NewExternalOneByte(
isolate: *mut Isolate,
onebyte_const: *const OneByteConst,
) -> *const String;

fn v8__String__NewExternalOneByteStatic(
isolate: *mut Isolate,
buffer: *const char,
Expand All @@ -90,6 +95,102 @@ extern "C" {
fn v8__String__ContainsOnlyOneByte(this: *const String) -> bool;
}

#[repr(C)]
#[derive(Debug)]
pub struct OneByteConst {
vtable: *const OneByteConstNoOp,
cached_data: *const char,
length: int,
}

// SAFETY: The vtable for OneByteConst is an immutable static and all
// of the included functions are thread-safe, the cached_data pointer
// is never changed and points to a static ASCII string, and the
// length is likewise never changed. Thus, it is safe to share the
// OneByteConst across threads. This means that multiple isolates
// can use the same OneByteConst statics simultaneously.
unsafe impl Sync for OneByteConst {}

extern "C" fn one_byte_const_no_op(_this: *const OneByteConst) {}
extern "C" fn one_byte_const_is_cacheable(_this: *const OneByteConst) -> bool {
true
}
extern "C" fn one_byte_const_data(this: *const OneByteConst) -> *const char {
// SAFETY: Only called from C++ with a valid OneByteConst pointer.
unsafe { (*this).cached_data }
}
extern "C" fn one_byte_const_length(this: *const OneByteConst) -> usize {
// SAFETY: Only called from C++ with a valid OneByteConst pointer.
unsafe { (*this).length as usize }
}

type OneByteConstNoOp = extern "C" fn(*const OneByteConst);
type OneByteConstIsCacheable = extern "C" fn(*const OneByteConst) -> bool;
type OneByteConstData = extern "C" fn(*const OneByteConst) -> *const char;
type OneByteConstLength = extern "C" fn(*const OneByteConst) -> usize;

#[repr(C)]
struct OneByteConstVtable {
#[cfg(target_family = "windows")]
// In SysV / Itanium ABI -0x10 offset of the vtable
// tells how many bytes the vtable pointer pointing to
// this vtable is offset from the base class. For
// single inheritance this is always 0.
_offset_to_top: usize,
// In Itanium ABI the -0x08 offset contains the type_info
// pointer, and in MSVC it contains the RTTI Complete Object
// Locator pointer. V8 is normally compiled with `-fno-rtti`
// meaning that this pointer is a nullptr on both
// Itanium and MSVC.
_typeinfo: *const (),
// After the metadata fields come the virtual function
// pointers. The vtable pointer in a class instance points
// to the first virtual function pointer, making this
// the 0x00 offset of the table.
// The order of the virtual function pointers is determined
// by their order of declaration in the classes.
delete1: OneByteConstNoOp,
// In SysV / Itanium ABI, a class vtable includes the
// deleting destructor and the compete object destructor.
// In MSVC, it only includes the deleting destructor.
#[cfg(not(target_family = "windows"))]
delete2: OneByteConstNoOp,
is_cacheable: OneByteConstIsCacheable,
dispose: OneByteConstNoOp,
lock: OneByteConstNoOp,
unlock: OneByteConstNoOp,
data: OneByteConstData,
length: OneByteConstLength,
}

const ONE_BYTE_CONST_VTABLE: OneByteConstVtable = OneByteConstVtable {
#[cfg(target_family = "windows")]
_offset_to_top: 0,
_typeinfo: std::ptr::null(),
delete1: one_byte_const_no_op,
#[cfg(not(target_family = "windows"))]
delete2: one_byte_const_no_op,
is_cacheable: one_byte_const_is_cacheable,
dispose: one_byte_const_no_op,
lock: one_byte_const_no_op,
unlock: one_byte_const_no_op,
data: one_byte_const_data,
length: one_byte_const_length,
};

/// Compile-time function to determine if a string is ASCII. Note that UTF-8 chars
/// longer than one byte have the high-bit set and thus, are not ASCII.
const fn is_ascii(s: &'static [u8]) -> bool {
let mut i = 0;
while i < s.len() {
if !s[i].is_ascii() {
return false;
}
i += 1;
}
true
}

#[repr(C)]
#[derive(Debug, Default)]
pub enum NewStringType {
Expand Down Expand Up @@ -332,6 +433,34 @@ impl String {
Self::new_from_utf8(scope, value.as_ref(), NewStringType::Normal)
}

// Compile-time function to create an external string resource.
// The buffer is checked to contain only ASCII characters.
#[inline(always)]
pub const fn create_external_onebyte_const(
buffer: &'static [u8],
) -> OneByteConst {
is_ascii(buffer);
OneByteConst {
vtable: &ONE_BYTE_CONST_VTABLE.delete1,
cached_data: buffer.as_ptr() as *const char,
length: buffer.len() as i32,
}
}

// Creates a v8::String from a `&'static OneByteConst`
// which is guaranteed to be Latin-1 or ASCII.
#[inline(always)]
pub fn new_from_onebyte_const<'s>(
scope: &mut HandleScope<'s, ()>,
onebyte_const: &'static OneByteConst,
) -> Option<Local<'s, String>> {
unsafe {
scope.cast_local(|sd| {
v8__String__NewExternalOneByte(sd.get_isolate_ptr(), onebyte_const)
})
}
}

// Creates a v8::String from a `&'static [u8]`,
// must be Latin-1 or ASCII, not UTF-8 !
#[inline(always)]
Expand Down
Loading

0 comments on commit 308a113

Please sign in to comment.