Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* vim: set ts=8 sts=2 et sw=2 tw=80:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "wasm/WasmStructLayout.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/HashFunctions.h"
#include "jstypes.h" // RoundUp
// This is a simple implementation of a layouter. It places the OOL pointer
// at the start of the IL payload area, regardless of whether an OOL area is
// actually necessary.
namespace js::wasm {
//=========================================================================
// BitVector
// See comment in WasmStructLayout.h for meaning of "byte", "offset" and
// "chunk".
#ifdef DEBUG
static bool Is8Aligned(uint32_t n) { return (n & 7) == 0; }
static bool IsWordAligned(uintptr_t x) { return (x % sizeof(void*)) == 0; }
#endif
static uint32_t IndexOfLeastSignificantZeroBit(uint8_t n) {
for (uint32_t i = 0; i < 8; i++) {
if (((n >> i) & 1) == 0) {
return i;
}
}
MOZ_CRASH();
}
static uint32_t IndexOfLeastSignificantZero2Bits(uint8_t n) {
for (uint32_t i = 0; i < 8; i += 2) {
if (((n >> i) & 3) == 0) {
return i;
}
}
MOZ_CRASH();
}
static uint32_t IndexOfLeastSignificantZero4Bits(uint8_t n) {
for (uint32_t i = 0; i < 8; i += 4) {
if (((n >> i) & 0xF) == 0) {
return i;
}
}
MOZ_CRASH();
}
static uint32_t IndexOfMostSignificantOneBit(uint8_t n) {
for (int32_t i = 7; i >= 0; i--) {
if (((n >> i) & 1) == 1) {
return uint32_t(i);
}
}
MOZ_CRASH();
}
#ifdef DEBUG
static uint32_t OffsetToChunkNumber(uint32_t offset) { return offset / 8; }
#endif
uint32_t BitVector::hashNonZero() const {
mozilla::HashNumber hash(42);
for (uint8_t b : chunks_) {
if (b != 0) {
hash = mozilla::AddToHash(hash, b);
}
}
return uint32_t(hash);
}
uint32_t BitVector::totalOffset() const {
if (chunks_.empty()) {
return 0;
}
// Find the highest non-zero chunk.
size_t i;
for (i = chunks_.length(); i >= 1; i--) {
if (chunks_[i - 1] != 0) {
break;
}
}
if (i == 0) {
// There are chunks, but none got used.
return 0;
}
i--;
MOZ_ASSERT(i < chunks_.length());
return 8 * uint32_t(i) + IndexOfMostSignificantOneBit(chunks_[i]) + 1;
}
BitVector::Result BitVector::addMoreChunks() {
for (uint32_t i = 0; i < LookbackLimit / 2; i++) {
if (!chunks_.append(0)) {
return Result::OOM;
}
}
return Result::OK;
}
BitVector::Result BitVector::init(uint32_t chunksReserved,
uint32_t chunksTotal) {
MOZ_ASSERT_IF(chunksReserved > 0, chunksReserved < chunksTotal);
if (!chunks_.resize(chunksTotal)) {
return Result::OOM;
}
for (uint32_t i = 0; i < chunksReserved; i++) {
chunks_[i] = 0xFF;
}
for (uint32_t i = chunksReserved; i < chunksTotal; i++) {
chunks_[i] = 0;
}
return Result::OK;
}
BitVector::Result BitVector::allocate(uint32_t size, uint32_t firstChunk,
uint32_t lastChunkPlus1,
uint32_t* offset) {
MOZ_ASSERT(firstChunk < lastChunkPlus1);
MOZ_ASSERT(lastChunkPlus1 <= chunks_.length());
// We don't want to re-scan the entire vector every search; that's
// expensive (quadratic). Instead just re-scan the last 24 chunks and
// accept that we'll miss out on the opportunity to use alignment holes
// more than 192 bytes back from the current "fill point" for the struct.
if (lastChunkPlus1 - firstChunk > LookbackLimit) {
firstChunk = lastChunkPlus1 - LookbackLimit;
}
// These are arranged in order of conceptually-simplest first.
switch (size) {
case 8: {
// Any chunk that is zero will do.
for (uint32_t i = firstChunk; i < lastChunkPlus1; i++) {
if (chunks_[i] == 0) {
*offset = i * 8;
chunks_[i] = 0xFF;
return Result::OK;
}
}
break;
}
case 16: {
// Any chunk-pair that is zero will do. Note this 8-aligns 16-byte
// requests, but we can't avoid that because the underlying JS heap
// allocator only provides 8-aligned addresses anyway.
for (uint32_t i = firstChunk + 1; i < lastChunkPlus1; i++) {
if (chunks_[i - 1] == 0 && chunks_[i] == 0) {
*offset = (i - 1) * 8;
chunks_[i - 1] = 0xFF;
chunks_[i] = 0xFF;
return Result::OK;
}
}
break;
}
// The 4, 2 and 1-byte cases are the most complex. We have to find a
// single chunk with that many consecutive, aligned bits, as zero.
case 1: {
// Any chunk that has an unset bit is fine.
for (uint32_t i = firstChunk; i < lastChunkPlus1; i++) {
if (chunks_[i] != 0xFF) {
uint32_t bitShift = IndexOfLeastSignificantZeroBit(chunks_[i]);
*offset = i * 8 + bitShift;
chunks_[i] |= (1 << bitShift);
return Result::OK;
}
}
break;
}
case 4: {
// Find a chunk in which either the upper or lower half is zero.
for (uint32_t i = firstChunk; i < lastChunkPlus1; i++) {
if ((chunks_[i] & (0xF << 0)) == 0 || (chunks_[i] & (0xF << 4)) == 0) {
uint32_t bitShift = IndexOfLeastSignificantZero4Bits(chunks_[i]);
*offset = i * 8 + bitShift;
chunks_[i] |= (0x0F << bitShift);
return Result::OK;
}
}
break;
}
case 2: {
// Find a chunk in which an adjacent bit-pair is zero.
for (uint32_t i = firstChunk; i < lastChunkPlus1; i++) {
if ((chunks_[i] & (3 << 0)) == 0 || (chunks_[i] & (3 << 2)) == 0 ||
(chunks_[i] & (3 << 4)) == 0 || (chunks_[i] & (3 << 6)) == 0) {
uint32_t bitShift = IndexOfLeastSignificantZero2Bits(chunks_[i]);
*offset = i * 8 + bitShift;
chunks_[i] |= (3 << bitShift);
return Result::OK;
}
}
break;
}
default: {
MOZ_CRASH();
}
}
return Result::Fail;
}
// Given that `offset` was allocated by a call to `allocate`
// (requesting size `size`), free up that area.
void BitVector::deallocate(uint32_t offset, uint32_t size) {
MOZ_ASSERT(OffsetToChunkNumber(offset + size - 1) < chunks_.length());
switch (size) {
case 8: {
MOZ_ASSERT((offset % 8) == 0);
uint32_t chunk = offset / 8;
MOZ_ASSERT(chunks_[chunk] == 0xFF);
chunks_[chunk] = 0;
break;
}
case 16: {
MOZ_ASSERT((offset % 8) == 0); // re 8, see comment on ::allocate
uint32_t chunk = offset / 8;
MOZ_ASSERT(chunk + 1 < chunks_.length());
MOZ_ASSERT(chunks_[chunk] == 0xFF);
MOZ_ASSERT(chunks_[chunk + 1] == 0xFF);
chunks_[chunk] = 0;
chunks_[chunk + 1] = 0;
break;
}
case 1: {
uint32_t chunk = offset / 8;
uint32_t shift = offset % 8; // 0, 1, 2, 3, 4, 5, 6 or 7
uint8_t mask = 1 << shift;
MOZ_ASSERT((chunks_[chunk] & mask) == mask);
chunks_[chunk] &= ~mask;
break;
}
case 4: {
MOZ_ASSERT((offset % 4) == 0);
uint32_t chunk = offset / 8;
uint32_t shift = offset % 8; // 0 or 4
uint8_t mask = 0xF << shift;
MOZ_ASSERT((chunks_[chunk] & mask) == mask);
chunks_[chunk] &= ~mask;
break;
}
case 2: {
MOZ_ASSERT((offset % 2) == 0);
uint32_t chunk = offset / 8;
uint32_t shift = offset % 8; // 0, 2, 4 or 6
uint8_t mask = 0x3 << shift;
MOZ_ASSERT((chunks_[chunk] & mask) == mask);
chunks_[chunk] &= ~mask;
break;
}
default: {
MOZ_CRASH();
}
}
}
//=========================================================================
// FixedSizeBitVector
BitVector::Result FixedSizeBitVector::init(uint32_t layoutBytesReserved,
uint32_t layoutBytesTotal) {
MOZ_ASSERT(layoutBytesTotal > 0);
MOZ_ASSERT(layoutBytesReserved < layoutBytesTotal);
MOZ_ASSERT(Is8Aligned(layoutBytesReserved));
MOZ_ASSERT(Is8Aligned(layoutBytesTotal));
chunksReserved_ = layoutBytesReserved / 8;
chunksTotal_ = layoutBytesTotal / 8;
return BitVector::init(chunksReserved_, chunksTotal_);
}
BitVector::Result FixedSizeBitVector::allocate(uint32_t size,
uint32_t* offset) {
return BitVector::allocate(size, chunksReserved_, chunksTotal_, offset);
}
//=========================================================================
// VariableSizeBitVector
BitVector::Result VariableSizeBitVector::init() {
// This initial size of 1 is important in that it needs to be less than
// ::LookbackLimit.
return BitVector::init(0, 1 /*see ::unused()*/);
}
BitVector::Result VariableSizeBitVector::allocate(uint32_t size,
uint32_t* offset) {
// First, try to find it given the chunks we already have.
Result res = BitVector::allocate(size, 0, chunks_.length(), offset);
if (res == Result::OOM) {
return Result::OOM;
}
if (res == Result::OK) {
used_ = true;
return res;
}
// That failed, so add some more (uncommitted) chunks on the end of `chunks_`
// and try again. This second attempt *must* succeed since we can make the
// OOL block arbitrarily large.
res = addMoreChunks();
if (res == Result::OOM) {
return Result::OOM;
}
res = BitVector::allocate(size, 0, chunks_.length(), offset);
if (res == Result::OOM) {
return Result::OOM;
}
MOZ_RELEASE_ASSERT(res == Result::OK);
used_ = true;
return Result::OK;
}
bool VariableSizeBitVector::unused() const { return !used_; }
uint32_t VariableSizeBitVector::totalOffset() const {
uint32_t res = BitVector::totalOffset();
MOZ_ASSERT(used_ == (res > 0));
return res;
}
//=========================================================================
// StructLayout
bool StructLayout::init(uint32_t firstUsableILOffset, uint32_t usableILSize) {
// Not actually necessary, but it would be strange if this wasn't so.
MOZ_ASSERT(IsWordAligned(firstUsableILOffset));
MOZ_ASSERT(IsWordAligned(usableILSize));
// Must have at least enough space to hold the OOL pointer
MOZ_ASSERT(usableILSize >= sizeof(void*));
oolptrILO_ = InvalidOffset;
BitVector::Result res = ilBitVector_.init(firstUsableILOffset,
firstUsableILOffset + usableILSize);
if (res == BitVector::Result::OOM) {
return false;
}
res = oolBitVector_.init();
if (res == BitVector::Result::OOM) {
return false;
}
return true;
}
// Add a field of the specified size, and get back its access path. The two
// release assertions together guarantee that the maximum offset that could be
// generated is roughly `16 * js::wasm::MaxStructFields`, so there is no need
// to use checked integers in the layout computations.
bool StructLayout::addField(uint32_t fieldSize, FieldAccessPath* path) {
MOZ_ASSERT(fieldSize == 16 || fieldSize == 8 || fieldSize == 4 ||
fieldSize == 2 || fieldSize == 1);
// Guard against field-offset overflow.
numFieldsProcessed_++;
MOZ_RELEASE_ASSERT(numFieldsProcessed_ <= js::wasm::MaxStructFields);
MOZ_RELEASE_ASSERT(fieldSize <= 16);
*path = FieldAccessPath();
// This is complex. In between calls to ::addField, we maintain the
// following invariant:
//
// (0) If the OOL area is not in use, then it is possible to allocate the OOL
// pointer in the IL area.
//
// With that in place, the code below deals with 4 cases:
//
// (1) The OOL area is unused, and both the field and a dummy OOL pointer fit
// into the IL area. Allocate the field IL and leave the OOL area
// unused. Because we just established that a dummy OOL pointer fits in
// IL after the field, and because of (N) below, (0) is true after the
// call.
//
// (2) The OOL area is unused, but the field and dummy OOL pointer don't both
// fit in the IL area. We need to bring the OOL area into use. Allocate
// the OOL pointer in the IL area (which due to (0) cannot fail), and
// allocate the field in the OOL area. This is a one-time transitional
// case that separates multiple occurrences of (1) from multiple
// occurrences of (3) and (4). This means the OOL area is now in use, so
// (0) is trivially true after the call.
//
// (3) The OOL area is in use, but the field fits in the IL area anyways,
// presumably because it falls into an alignment hole in the IL area.
// Just allocate it IL and leave everything else unchanged. Since the
// OOL area is in use, (0) is trivially true after the call.
//
// (4) The OOL area is in use, and the field doesn't fit in the IL area.
// Allocate it in the OOL area. Since the OOL area is in use, (0) is
// trivially true after the call.
//
// (N) Note: for (1) and (2) it is important to try for the allocation of the
// field first and the dummy OOL pointer second.
// For cases (1) and (2) we need to back out tentative allocations. Hash the
// current state so we can later assert it is unchanged after backouts.
mozilla::DebugOnly<uint32_t> initialHash = hash();
// These need to agree.
MOZ_ASSERT(oolBitVector_.unused() == (oolptrILO_ == InvalidOffset));
// Try for Case (1)
if (oolBitVector_.unused()) {
uint32_t fieldOffset = InvalidOffset;
BitVector::Result res = ilBitVector_.allocate(fieldSize, &fieldOffset);
if (res == BitVector::Result::OOM) {
return false;
}
// The field fits, now try for the dummy OOL pointer
mozilla::DebugOnly<uint32_t> hash2 = hash();
if (res == BitVector::Result::OK) {
uint32_t dummyOffset = InvalidOffset;
res = ilBitVector_.allocate(sizeof(void*), &dummyOffset);
if (res == BitVector::Result::OOM) {
return false;
}
if (res == BitVector::Result::OK) {
// Case (1) established -- they both fit.
// Back out the dummy OOL pointer allocation, and we're done.
MOZ_ASSERT(fieldOffset != dummyOffset);
ilBitVector_.deallocate(dummyOffset, sizeof(void*));
MOZ_ASSERT(hash() == hash2);
*path = FieldAccessPath(fieldOffset);
return true;
}
// The field fits, but the OOL pointer doesn't. Back out the field
// allocation, so that we have changed nothing.
ilBitVector_.deallocate(fieldOffset, fieldSize);
}
}
// "state is unchanged from when we started"
MOZ_ASSERT(hash() == initialHash);
MOZ_ASSERT(oolBitVector_.unused() == (oolptrILO_ == InvalidOffset));
// Try for Case (2)
if (oolBitVector_.unused()) {
// We need to bring the OOL area into use. First, try to allocate the OOL
// pointer field. This must succeed (apart from OOMing) because of (1).
uint32_t oolptrOffset = InvalidOffset;
BitVector::Result res = ilBitVector_.allocate(sizeof(void*), &oolptrOffset);
if (res == BitVector::Result::OOM) {
return false;
}
MOZ_ASSERT(res == BitVector::Result::OK);
// Case (2) established
oolptrILO_ = oolptrOffset;
// Allocate the field in the OOL area; it is the first item there.
uint32_t fieldOffset = InvalidOffset;
res = oolBitVector_.allocate(fieldSize, &fieldOffset);
if (res == BitVector::Result::OOM) {
return false;
}
// Allocation in the OOL area can't fail.
MOZ_RELEASE_ASSERT(res == BitVector::Result::OK);
MOZ_ASSERT(!oolBitVector_.unused());
// We expect this because this is the first item in the OOL area.
MOZ_ASSERT(fieldOffset == 0);
*path = FieldAccessPath(oolptrILO_, fieldOffset);
return true;
}
// "state is unchanged from when we started"
MOZ_ASSERT(hash() == initialHash);
MOZ_ASSERT(!oolBitVector_.unused() && oolptrILO_ != InvalidOffset);
// Cases (3) and (4). In both cases, the OOL area is in use.
// Re-try allocating the field IL. Note this is not redundant w.r.t. the
// logic above, since that involved trying to allocate both the field and
// the dummy OOL pointer; this only tries to allocate the field.
uint32_t fieldOffset = InvalidOffset;
BitVector::Result res = ilBitVector_.allocate(fieldSize, &fieldOffset);
if (res == BitVector::Result::OOM) {
return false;
}
if (res == BitVector::Result::OK) {
// Case (3) established
*path = FieldAccessPath(fieldOffset);
return true;
}
// Case (4) established
fieldOffset = InvalidOffset;
res = oolBitVector_.allocate(fieldSize, &fieldOffset);
if (res == BitVector::Result::OOM) {
return false;
}
// Allocation in the OOL area can't fail.
MOZ_RELEASE_ASSERT(res == BitVector::Result::OK);
*path = FieldAccessPath(oolptrILO_, fieldOffset);
return true;
}
uint32_t StructLayout::hash() const {
uint32_t h = ilBitVector_.hashNonZero();
h = (h << 16) | (h >> 16);
h ^= oolBitVector_.hashNonZero();
return h;
}
uint32_t StructLayout::totalSizeIL() const {
return js::RoundUp(ilBitVector_.totalOffset(), sizeof(void*));
}
bool StructLayout::hasOOL() const { return !oolBitVector_.unused(); }
uint32_t StructLayout::totalSizeOOL() const {
MOZ_ASSERT(hasOOL());
MOZ_ASSERT(oolptrILO_ != InvalidOffset);
return js::RoundUp(oolBitVector_.totalOffset(), sizeof(void*));
}
FieldAccessPath StructLayout::oolPointerPath() const {
MOZ_ASSERT(hasOOL());
MOZ_ASSERT(oolptrILO_ != InvalidOffset);
return FieldAccessPath(oolptrILO_);
}
} // namespace js::wasm