using mozilla::CheckedUint32; using mozilla::DebugOnly; using mozilla::Maybe; using mozilla::Nothing; using mozilla::Some;
// Instance must be aligned at least as much as any of the integer, float, // or SIMD values that we'd like to store in it.
static_assert(alignof(Instance) >=
std::max(sizeof(Registers::RegisterContent), sizeof(FloatRegisters::RegisterContent)));
// The globalArea must be aligned at least as much as an instance. This is // guaranteed to be sufficient for all data types we care about, including // SIMD values. See the above assertion.
static_assert(Instance::offsetOfData() % alignof(Instance) == 0);
// We want the memory base to be the first field, and accessible with no // offset. This incidentally is also an assertion that there is no superclass // with fields.
static_assert(Instance::offsetOfMemory0Base() == 0);
// We want instance fields that are commonly accessed by the JIT to have // compact encodings. A limit of less than 128 bytes is chosen to fit within // the signed 8-bit mod r/m x86 encoding.
static_assert(Instance::offsetOfLastCommonJitField() < 128);
////////////////////////////////////////////////////////////////////////////// // // Functions and invocation.
constvoid* Instance::addressOfGlobalCell(const GlobalDesc& global) const { constvoid* cell = data() + global.offset(); // Indirect globals store a pointer to their cell in the instance global // data. Dereference it to find the real cell. if (global.isIndirect()) {
cell = *(constvoid**)cell;
} return cell;
}
staticbool UnpackResults(JSContext* cx, const ValTypeVector& resultTypes, const Maybe<char*> stackResultsArea, uint64_t* argv,
MutableHandleValue rval) { if (!stackResultsArea) {
MOZ_ASSERT(resultTypes.length() <= 1); // Result is either one scalar value to unpack to a wasm value, or // an ignored value for a zero-valued function. if (resultTypes.length() == 1) { return ToWebAssemblyValue(cx, rval, resultTypes[0], argv, true);
} returntrue;
}
MOZ_ASSERT(stackResultsArea.isSome());
Rooted<ArrayObject*> array(cx, IterableToArray(cx, rval)); if (!array) { returnfalse;
}
if (resultTypes.length() != array->length()) {
UniqueChars expected(JS_smprintf("%zu", resultTypes.length()));
UniqueChars got(JS_smprintf("%u", array->length())); if (!expected || !got) {
ReportOutOfMemory(cx); returnfalse;
}
ABIResultIter iter(ResultType::Vector(resultTypes)); // The values are converted in the order they are pushed on the // abstract WebAssembly stack; switch to iterate in push order. while (!iter.done()) {
iter.next();
}
DebugOnly<bool> seenRegisterResult = false; for (iter.switchToPrev(); !iter.done(); iter.prev()) { const ABIResult& result = iter.cur();
MOZ_ASSERT(!seenRegisterResult); // Use rval as a scratch area to hold the extracted result.
rval.set(array->getDenseElement(iter.index())); if (result.inRegister()) { // Currently, if a function type has results, there can be only // one register result. If there is only one result, it is // returned as a scalar and not an iterable, so we don't get here. // If there are multiple results, we extract the register result // and set `argv[0]` set to the extracted result, to be returned by // register in the stub. The register result follows any stack // results, so this preserves conversion order. if (!ToWebAssemblyValue(cx, rval, result.type(), argv, true)) { returnfalse;
}
seenRegisterResult = true; continue;
}
uint32_t result_size = result.size();
MOZ_ASSERT(result_size == 4 || result_size == 8); #ifdef DEBUG if (previousOffset == ~(uint64_t)0) {
previousOffset = (uint64_t)result.stackOffset();
} else {
MOZ_ASSERT(previousOffset - (uint64_t)result_size ==
(uint64_t)result.stackOffset());
previousOffset -= (uint64_t)result_size;
} #endif char* loc = stackResultsArea.value() + result.stackOffset(); if (!ToWebAssemblyValue(cx, rval, result.type(), loc, result_size == 8)) { returnfalse;
}
}
// If we're applying the Function.prototype.call.bind optimization, the // number of arguments to the target function is decreased by one to account // for the 'this' parameter we're passing bool isFunctionCallBind = instanceFuncImport.isFunctionCallBind; if (isFunctionCallBind) { // Guarded against in MaybeOptimizeFunctionCallBind.
MOZ_ASSERT(invokeArgsLength != 0);
invokeArgsLength -= 1;
}
MOZ_ASSERT(argTypes.lengthWithStackResults() == argc);
Maybe<char*> stackResultPointer;
size_t lastBoxIndexPlusOne = 0;
{
JS::AutoAssertNoGC nogc; for (size_t i = 0; i < argc; i++) { constvoid* rawArgLoc = &argv[i];
if (argTypes.isSyntheticStackResultPointerArg(i)) {
stackResultPointer = Some(*(char**)rawArgLoc); continue;
}
size_t naturalIndex = argTypes.naturalIndex(i);
ValType type = funcType.args()[naturalIndex];
// Skip JS value conversion that may GC (as the argument array is not // rooted), and do that in a follow up loop. if (ToJSValueMayGC(type)) {
lastBoxIndexPlusOne = i + 1; continue;
}
// Visit arguments that need to perform allocation in a second loop // after the rest of arguments are converted. for (size_t i = 0; i < lastBoxIndexPlusOne; i++) { if (argTypes.isSyntheticStackResultPointerArg(i)) { continue;
}
size_t naturalIndex = argTypes.naturalIndex(i);
ValType type = funcType.args()[naturalIndex];
// Visit the arguments that could trigger a GC now. if (!ToJSValueMayGC(type)) { continue;
} // All value types that require boxing when converted to a JS value are not // references.
MOZ_ASSERT(!type.isRefRepr());
// The conversions are safe here because source values are not references // and will not be moved. This may move the unrooted arguments in the array // but that's okay because those were handled in the above loop. constvoid* rawArgLoc = &argv[i];
MutableHandleValue argValue =
isFunctionCallBind
? ((naturalIndex == 0) ? &thisv : invokeArgs[naturalIndex - 1])
: invokeArgs[naturalIndex]; if (!ToJSValue(cx, rawArgLoc, type, argValue)) { returnfalse;
}
}
if (!UnpackResults(cx, funcType.results(), stackResultPointer, argv, &rval)) { returnfalse;
}
if (!JitOptions.enableWasmJitExit) { returntrue;
}
// JIT exits have not been updated to support the Function.prototype.call.bind // optimization. if (instanceFuncImport.isFunctionCallBind) { returntrue;
}
// The import may already have become optimized. const FuncImport& funcImport = code().funcImport(funcImportIndex); void* jitExitCode =
code().sharedStubs().base() + funcImport.jitExitCodeOffset(); if (instanceFuncImport.code == jitExitCode) { returntrue;
}
if (!importCallable->is<JSFunction>()) { returntrue;
}
// Test if the function is JIT compiled. if (!importCallable->as<JSFunction>().hasBytecode()) { returntrue;
}
JSScript* script = importCallable->as<JSFunction>().nonLazyScript(); if (!script->hasJitScript()) { returntrue;
}
// Skip if pushing arguments would require stack probes. if (importCallable->as<JSFunction>().nargs() > JIT_ARGS_LENGTH_MAX) { returntrue;
}
// Skip if the function does not have a signature that allows for a JIT exit. if (!funcType.canHaveJitExit()) { returntrue;
}
// It is safe to cast to uint32_t, as all limits have been checked inside // grow() and will not have been exceeded for a 32-bit memory.
uint32_t ret = uint32_t(WasmMemoryObject::grow(memory, uint64_t(delta), cx));
// If there has been a moving grow, this Instance should have been notified.
MOZ_RELEASE_ASSERT(
instance->memoryBase(memoryIndex) ==
instance->memory(memoryIndex)->buffer().dataPointerEither());
uint64_t ret = WasmMemoryObject::grow(memory, delta, cx);
// If there has been a moving grow, this Instance should have been notified.
MOZ_RELEASE_ASSERT(
instance->memoryBase(memoryIndex) ==
instance->memory(memoryIndex)->buffer().dataPointerEither());
// This invariant must hold when running Wasm code. Assert it here so we can // write tests for cross-realm calls.
DebugOnly<JSContext*> cx = instance->cx();
MOZ_ASSERT(cx->realm() == instance->realm());
Pages pages = instance->memory(memoryIndex)->volatilePages(); #ifdef JS_64BIT // Ensure that the memory size is no more than 4GiB.
MOZ_ASSERT(pages <=
Pages::fromPageCount(
MaxMemoryPagesValidation(AddressType::I32, pages.pageSize()),
pages.pageSize())); #endif return uint32_t(pages.pageCount());
}
// This invariant must hold when running Wasm code. Assert it here so we can // write tests for cross-realm calls.
DebugOnly<JSContext*> cx = instance->cx();
MOZ_ASSERT(cx->realm() == instance->realm());
// Dynamic dispatch to get the length of a memory given just the base and // whether it is shared or not. This is only used for memCopy_any, where being // slower is okay. staticinline size_t GetVolatileByteLength(uint8_t* memBase, bool isShared) { if (isShared) { return WasmSharedArrayRawBuffer::fromDataPtr(memBase)->volatileByteLength();
} return WasmArrayRawBuffer::fromDataPtr(memBase)->byteLength();
}
template <typename T, typename F, typename I> inline int32_t WasmMemoryFill(JSContext* cx, T memBase, size_t memLen,
I byteOffset, uint32_t value, I len, F memSet) { if (!MemoryBoundsCheck(byteOffset, len, memLen)) {
ReportTrapError(cx, JSMSG_WASM_OUT_OF_BOUNDS); return -1;
}
// The required write direction is upward, but that is not currently // observable as there are no fences nor any read/write protect operation.
memSet(memBase + uintptr_t(byteOffset), int(value), size_t(len)); return0;
}
// Besides the obvious, this condition also handles dropped data segments, // because any nonzero init length on a dropped segment will fail the above // bounds check. if (len == 0) { return0;
}
MOZ_RELEASE_ASSERT(maybeSeg); const DataSegment& seg = *maybeSeg;
MOZ_RELEASE_ASSERT(!seg.active());
// The required read/write direction is upward, but that is not currently // observable as there are no fences nor any read/write protect operation.
SharedMem<uint8_t*> dataPtr = mem->buffer().dataPointerEither(); if (mem->isShared()) {
AtomicOperations::memcpySafeWhenRacy(
dataPtr + uintptr_t(dstOffset), (uint8_t*)seg.bytes.begin() + srcOffset,
len);
} else {
uint8_t* rawBuf = dataPtr.unwrap(/*Unshared*/);
memcpy(rawBuf + uintptr_t(dstOffset),
(constchar*)seg.bytes.begin() + srcOffset, len);
} return0;
}
if (table.isFunction() &&
seg.encoding == ModuleElemSegment::Encoding::Indices) { // Initialize this table of functions without creating any intermediate // JSFunctions. bool ok = iterElemsFunctions(
seg, [&](uint32_t i, void* code, Instance* instance) -> bool {
table.setFuncRef(dstOffset + i, code, instance); returntrue;
}); if (!ok) { returnfalse;
}
} else { bool ok = iterElemsAnyrefs(cx, seg, [&](uint32_t i, AnyRef ref) -> bool {
table.setRef(dstOffset + i, ref); returntrue;
}); if (!ok) { returnfalse;
}
}
returntrue;
}
template <typename F> bool Instance::iterElemsFunctions(const ModuleElemSegment& seg, const F& onFunc) { // In the future, we could theoretically get function data (instance + code // pointer) from segments with the expression encoding without creating // JSFunctions. But that is not how it works today. We can only bypass the // creation of JSFunctions for the index encoding.
MOZ_ASSERT(seg.encoding == ModuleElemSegment::Encoding::Indices);
for (uint32_t i = 0; i < seg.numElements(); i++) {
uint32_t elemFuncIndex = seg.elemIndices[i];
if (elemFuncIndex < funcImports.length()) {
FuncImportInstanceData& import = funcImportInstanceData(elemFuncIndex);
MOZ_ASSERT(import.callable->isCallable());
if (import.callable->is<JSFunction>()) {
JSFunction* fun = &import.callable->as<JSFunction>(); if (!codeMeta().funcImportsAreJS && fun->isWasm()) { // Unwrapped Function.prototype.call.bind imports should not be used // when the unwrapped function is a wasm function.
MOZ_ASSERT(!import.isFunctionCallBind);
// This element is a wasm function imported from another // instance. To preserve the === function identity required by // the JS embedding spec, we must get the imported function's // underlying CodeRange.funcCheckedCallEntry and Instance so that // future Table.get()s produce the same function object as was // imported. if (!onFunc(i, fun->wasmCheckedCallEntry(), &fun->wasmInstance())) { returnfalse;
} continue;
}
}
}
switch (seg.encoding) { case ModuleElemSegment::Encoding::Indices: { // The only types of indices that exist right now are function indices, so // this code is specialized to functions.
RootedFunction fun(cx); for (uint32_t i = 0; i < seg.numElements(); i++) {
uint32_t funcIndex = seg.elemIndices[i]; if (!getExportedFunction(cx, funcIndex, &fun) ||
!onAnyRef(i, AnyRef::fromJSObject(*fun.get()))) { returnfalse;
}
} break;
} case ModuleElemSegment::Encoding::Expressions: {
Rooted<WasmInstanceObject*> instanceObj(cx, object()); const ModuleElemSegment::Expressions& exprs = seg.elemExpressions;
UniqueChars error; // The offset is a dummy because the expression has already been // validated.
Decoder d(exprs.exprBytes.begin(), exprs.exprBytes.end(), 0, &error); for (uint32_t i = 0; i < seg.numElements(); i++) {
RootedVal result(cx); if (!InitExpr::decodeAndEvaluate(cx, instanceObj, d, seg.elemType,
&result)) {
MOZ_ASSERT(!error); // The only possible failure should be OOM. returnfalse;
} // We would need to root this AnyRef if we were doing anything other // than storing it.
AnyRef ref = result.get().ref(); if (!onAnyRef(i, ref)) { returnfalse;
}
} break;
} default:
MOZ_CRASH("unknown encoding type for element segment");
} returntrue;
}
////////////////////////////////////////////////////////////////////////////// // // GC and exception handling support.
/* static */ template <bool ZeroFields> void* Instance::structNewIL(Instance* instance, uint32_t typeDefIndex,
gc::AllocSite* allocSite) {
MOZ_ASSERT((ZeroFields ? SASigStructNewIL_true : SASigStructNewIL_false)
.failureMode == FailureMode::FailOnNullPtr);
JSContext* cx = instance->cx();
TypeDefInstanceData* typeDefData =
instance->typeDefInstanceData(typeDefIndex); // The new struct will be allocated in an initial heap as determined by // pretenuring logic as set up in `Instance::init`. return WasmStructObject::createStructIL<ZeroFields>(
cx, typeDefData, allocSite, allocSite->initialHeap());
}
/* static */ template <bool ZeroFields> void* Instance::structNewOOL(Instance* instance, uint32_t typeDefIndex,
gc::AllocSite* allocSite) {
MOZ_ASSERT((ZeroFields ? SASigStructNewOOL_true : SASigStructNewOOL_false)
.failureMode == FailureMode::FailOnNullPtr);
JSContext* cx = instance->cx();
TypeDefInstanceData* typeDefData =
instance->typeDefInstanceData(typeDefIndex); // The new struct will be allocated in an initial heap as determined by // pretenuring logic as set up in `Instance::init`. return WasmStructObject::createStructOOL<ZeroFields>(
cx, typeDefData, allocSite, allocSite->initialHeap());
}
// Copies from a (possibly dropped) data segment into a wasm GC array. Performs // the necessary bounds checks, accounting for the array's element size. If this // function returns false, it has already reported a trap error. Null arrays // should be handled in the caller. staticbool ArrayCopyFromData(JSContext* cx, Handle<WasmArrayObject*> arrayObj,
uint32_t arrayIndex, const DataSegment* seg,
uint32_t segByteOffset, uint32_t numElements) {
uint32_t elemSize = arrayObj->typeDef().arrayType().elementType().size();
// Compute the number of bytes to copy, ensuring it's below 2^32.
CheckedUint32 numBytesToCopy =
CheckedUint32(numElements) * CheckedUint32(elemSize); if (!numBytesToCopy.isValid()) { // If 2^32 or more bytes are to be copied, this is necessarily out of bounds // on the data segment.
ReportTrapError(cx, JSMSG_WASM_OUT_OF_BOUNDS); returnfalse;
}
// Range-check the copy. The obvious thing to do is to compute the offset // of the last byte to copy, but that would cause underflow in the // zero-length-and-zero-offset case. Instead, compute that value plus one; // in other words the offset of the first byte *not* to copy.
CheckedUint32 lastByteOffsetPlus1 =
CheckedUint32(segByteOffset) + numBytesToCopy;
CheckedUint32 numBytesAvailable(seg ? seg->bytes.length() : 0); if (!lastByteOffsetPlus1.isValid() || !numBytesAvailable.isValid() ||
lastByteOffsetPlus1.value() > numBytesAvailable.value()) { // Because the last byte to copy doesn't exist inside `seg->bytes`.
ReportTrapError(cx, JSMSG_WASM_OUT_OF_BOUNDS); returnfalse;
}
// Range check the destination array.
uint64_t dstNumElements = uint64_t(arrayObj->numElements_); if (uint64_t(arrayIndex) + uint64_t(numElements) > dstNumElements) {
ReportTrapError(cx, JSMSG_WASM_OUT_OF_BOUNDS); returnfalse;
}
// This value is safe due to the previous range check on number of elements. // (We know the full result fits in the array, and we can't overflow uint64_t // since elemSize caps out at 16.)
uint64_t dstByteOffset = uint64_t(arrayIndex) * uint64_t(elemSize);
// Because `numBytesToCopy` is an in-range `CheckedUint32`, the cast to // `size_t` is safe even on a 32-bit target. if (numElements != 0) {
MOZ_RELEASE_ASSERT(seg);
memcpy(&arrayObj->data_[dstByteOffset], &seg->bytes[segByteOffset],
size_t(numBytesToCopy.value()));
}
returntrue;
}
// Copies from an element segment into a wasm GC array. Performs the necessary // bounds checks, accounting for the array's element size. If this function // returns false, it has already reported a trap error. staticbool ArrayCopyFromElem(JSContext* cx, Handle<WasmArrayObject*> arrayObj,
uint32_t arrayIndex, const InstanceElemSegment& seg,
uint32_t segOffset, uint32_t numElements) { // Range-check the copy. As in ArrayCopyFromData, compute the index of the // last element to copy, plus one.
CheckedUint32 lastIndexPlus1 =
CheckedUint32(segOffset) + CheckedUint32(numElements);
CheckedUint32 numElemsAvailable(seg.length()); if (!lastIndexPlus1.isValid() || !numElemsAvailable.isValid() ||
lastIndexPlus1.value() > numElemsAvailable.value()) { // Because the last element to copy doesn't exist inside the segment.
ReportTrapError(cx, JSMSG_WASM_OUT_OF_BOUNDS); returnfalse;
}
// Range check the destination array.
uint64_t dstNumElements = uint64_t(arrayObj->numElements_); if (uint64_t(arrayIndex) + uint64_t(numElements) > dstNumElements) {
ReportTrapError(cx, JSMSG_WASM_OUT_OF_BOUNDS); returnfalse;
}
// Creates an array (WasmArrayObject) containing `numElements` of type // described by `typeDef`. Initialises it with data copied from the data // segment whose index is `segIndex`, starting at byte offset `segByteOffset` // in the segment. Traps if the segment doesn't hold enough bytes to fill the // array. /* static */ void* Instance::arrayNewData(
Instance* instance, uint32_t segByteOffset, uint32_t numElements,
uint32_t typeDefIndex, gc::AllocSite* allocSite, uint32_t segIndex) {
MOZ_ASSERT(SASigArrayNewData.failureMode == FailureMode::FailOnNullPtr);
JSContext* cx = instance->cx();
TypeDefInstanceData* typeDefData =
instance->typeDefInstanceData(typeDefIndex);
// Check that the data segment is valid for use.
MOZ_RELEASE_ASSERT(size_t(segIndex) < instance->passiveDataSegments_.length(), "ensured by validation"); const DataSegment* seg = instance->passiveDataSegments_[segIndex];
Rooted<WasmArrayObject*> arrayObj(
cx,
WasmArrayObject::createArray<true>(
cx, typeDefData, allocSite, allocSite->initialHeap(), numElements)); if (!arrayObj) { // WasmArrayObject::createArray will have reported OOM. return nullptr;
}
MOZ_RELEASE_ASSERT(arrayObj->is<WasmArrayObject>());
if (!ArrayCopyFromData(cx, arrayObj, 0, seg, segByteOffset, numElements)) { // Trap errors will be reported by ArrayCopyFromData. return nullptr;
}
return arrayObj;
}
// This is almost identical to ::arrayNewData, apart from the final part that // actually copies the data. It creates an array (WasmArrayObject) // containing `numElements` of type described by `typeDef`. Initialises it // with data copied from the element segment whose index is `segIndex`, // starting at element number `srcOffset` in the segment. Traps if the // segment doesn't hold enough elements to fill the array. /* static */ void* Instance::arrayNewElem(
Instance* instance, uint32_t srcOffset, uint32_t numElements,
uint32_t typeDefIndex, gc::AllocSite* allocSite, uint32_t segIndex) {
MOZ_ASSERT(SASigArrayNewElem.failureMode == FailureMode::FailOnNullPtr);
JSContext* cx = instance->cx();
TypeDefInstanceData* typeDefData =
instance->typeDefInstanceData(typeDefIndex);
// Check that the element segment is valid for use.
MOZ_RELEASE_ASSERT(size_t(segIndex) < instance->passiveElemSegments_.length(), "ensured by validation"); const InstanceElemSegment& seg = instance->passiveElemSegments_[segIndex];
constTypeDef* typeDef = typeDefData->typeDef;
// Any data coming from an element segment will be an AnyRef. Writes into // array memory are done with raw pointers, so we must ensure here that the // destination size is correct.
MOZ_RELEASE_ASSERT(typeDef->arrayType().elementType().size() == sizeof(AnyRef));
Rooted<WasmArrayObject*> arrayObj(
cx,
WasmArrayObject::createArray<true>(
cx, typeDefData, allocSite, allocSite->initialHeap(), numElements)); if (!arrayObj) { // WasmArrayObject::createArray will have reported OOM. return nullptr;
}
MOZ_RELEASE_ASSERT(arrayObj->is<WasmArrayObject>());
if (!ArrayCopyFromElem(cx, arrayObj, 0, seg, srcOffset, numElements)) { // Trap errors will be reported by ArrayCopyFromElems. return nullptr;
}
return arrayObj;
}
// Copies a range of the data segment `segIndex` into an array // (WasmArrayObject), starting at offset `segByteOffset` in the data segment and // index `index` in the array. `numElements` is the length of the copy in array // elements, NOT bytes - the number of bytes will be computed based on the type // of the array. // // Traps if accesses are out of bounds for either the data segment or the array, // or if the array object is null. /* static */ int32_t Instance::arrayInitData(Instance* instance, void* array,
uint32_t index,
uint32_t segByteOffset,
uint32_t numElements,
uint32_t segIndex) {
MOZ_ASSERT(SASigArrayInitData.failureMode == FailureMode::FailOnNegI32);
JSContext* cx = instance->cx();
// Check that the data segment is valid for use.
MOZ_RELEASE_ASSERT(size_t(segIndex) < instance->passiveDataSegments_.length(), "ensured by validation"); const DataSegment* seg = instance->passiveDataSegments_[segIndex];
// Trap if the array is null. if (!array) {
ReportTrapError(cx, JSMSG_WASM_DEREF_NULL); return -1;
}
// Get hold of the array.
Rooted<WasmArrayObject*> arrayObj(cx, static_cast<WasmArrayObject*>(array));
MOZ_RELEASE_ASSERT(arrayObj->is<WasmArrayObject>());
if (!ArrayCopyFromData(cx, arrayObj, index, seg, segByteOffset,
numElements)) { // Trap errors will be reported by ArrayCopyFromData. return -1;
}
return0;
}
// Copies a range of the element segment `segIndex` into an array // (WasmArrayObject), starting at offset `segOffset` in the elem segment and // index `index` in the array. `numElements` is the length of the copy. // // Traps if accesses are out of bounds for either the elem segment or the array, // or if the array object is null. /* static */ int32_t Instance::arrayInitElem(Instance* instance, void* array,
uint32_t index, uint32_t segOffset,
uint32_t numElements,
uint32_t typeDefIndex,
uint32_t segIndex) {
MOZ_ASSERT(SASigArrayInitElem.failureMode == FailureMode::FailOnNegI32);
JSContext* cx = instance->cx();
// Check that the element segment is valid for use.
MOZ_RELEASE_ASSERT(size_t(segIndex) < instance->passiveElemSegments_.length(), "ensured by validation"); const InstanceElemSegment& seg = instance->passiveElemSegments_[segIndex];
// Trap if the array is null. if (!array) {
ReportTrapError(cx, JSMSG_WASM_DEREF_NULL); return -1;
}
// Any data coming from an element segment will be an AnyRef. Writes into // array memory are done with raw pointers, so we must ensure here that the // destination size is correct.
DebugOnly<constTypeDef*> typeDef =
&instance->codeMeta().types->type(typeDefIndex);
MOZ_ASSERT(typeDef->arrayType().elementType().size() == sizeof(AnyRef));
// Get hold of the array.
Rooted<WasmArrayObject*> arrayObj(cx, static_cast<WasmArrayObject*>(array));
MOZ_RELEASE_ASSERT(arrayObj->is<WasmArrayObject>());
if (!ArrayCopyFromElem(cx, arrayObj, index, seg, segOffset, numElements)) { // Trap errors will be reported by ArrayCopyFromElems. return -1;
}
return0;
}
// Copies range of elements between two arrays. // // Traps if accesses are out of bounds for the arrays, or either array // object is null. // // This function is only used by baseline, Ion emits inline code using // WasmArrayMemMove and WasmArrayRefsMove builtins instead. /* static */ int32_t Instance::arrayCopy(Instance* instance, void* dstArray,
uint32_t dstIndex, void* srcArray,
uint32_t srcIndex,
uint32_t numElements,
uint32_t elementSize) {
MOZ_ASSERT(SASigArrayCopy.failureMode == FailureMode::FailOnNegI32);
// At the entry point, `elementSize` may be negative to indicate // reftyped-ness of array elements. That is done in order to avoid having // to pass yet another (boolean) parameter here.
// "traps if either array is null" if (!srcArray || !dstArray) {
ReportTrapError(instance->cx(), JSMSG_WASM_DEREF_NULL); return -1;
}
// Get hold of the two arrays.
WasmArrayObject* dstArrayObj = static_cast<WasmArrayObject*>(dstArray);
WasmArrayObject* srcArrayObj = static_cast<WasmArrayObject*>(srcArray);
MOZ_ASSERT(dstArrayObj->is<WasmArrayObject>() &&
srcArrayObj->is<WasmArrayObject>());
// If WasmArrayObject::numElements() is changed to return 64 bits, the // following checking logic will be incorrect.
STATIC_ASSERT_WASMARRAYELEMENTS_NUMELEMENTS_IS_U32;
// "traps if destination + length > len(array1)"
uint64_t dstNumElements = uint64_t(dstArrayObj->numElements_); if (uint64_t(dstIndex) + uint64_t(numElements) > dstNumElements) { // Potential GC hazard: srcArrayObj and dstArrayObj are invalidated by // reporting an error, do no use them after this point.
ReportTrapError(instance->cx(), JSMSG_WASM_OUT_OF_BOUNDS); return -1;
}
// "traps if source + length > len(array2)"
uint64_t srcNumElements = uint64_t(srcArrayObj->numElements_); if (uint64_t(srcIndex) + uint64_t(numElements) > srcNumElements) { // Potential GC hazard: srcArrayObj and dstArrayObj are invalidated by // reporting an error, do no use them after this point.
ReportTrapError(instance->cx(), JSMSG_WASM_OUT_OF_BOUNDS); return -1;
}
if (numElements == 0) { // Early exit if there's no work to do. return0;
}
// Actually do the copy, taking care to handle cases where the src and dst // areas overlap.
uint8_t* srcBase = srcArrayObj->data_;
uint8_t* dstBase = dstArrayObj->data_;
srcBase += size_t(srcIndex) * size_t(elementSize);
dstBase += size_t(dstIndex) * size_t(elementSize); if (srcBase == dstBase) { // Early exit if there's no work to do. return0;
}
if (!elemsAreRefTyped) { // Hand off to memmove, which is presumably highly optimized.
memmove(dstBase, srcBase, size_t(numElements) * size_t(elementSize)); return0;
}
// We don't create the .stack property by default, unless the pref is set for // debugging. if (JS::Prefs::wasm_exception_force_stack_trace() &&
!CaptureStack(cx, &stack, MAX_REPORTED_STACK_DEPTH)) {
ReportOutOfMemory(cx); return nullptr;
}
// An OOM will result in null which will be caught on the wasm side. return AnyRef::fromJSObjectOrNull(
WasmExceptionObject::create(cx, tagObj, stack, proto))
.forCompiledCode();
}
// Check for any error conditions before calling fromCodePoint so we report // the correct error if (codePoint > unicode::NonBMPMax) {
ReportTrapError(cx, JSMSG_WASM_BAD_CODEPOINT); return nullptr;
}
if (result < 0) { return -1;
} if (result > 0) { return1;
} return result;
}
void Instance::addSubI128(Instance* instance, uint32_t isAdd) { #ifndef JS_64BIT
static_assert(sizeof(instance->baselineScratchWords_[0]) == sizeof(uint32_t));
static_assert(N_BASELINE_SCRATCH_WORDS >= 8); // Compute // // baselineScratchWords_[3:0] += baselineScratchWords_[7:4] (isAdd[0] == 1) // or // baselineScratchWords_[3:0] -= baselineScratchWords_[7:4] (isAdd[0] == 0) // // where entries [3:0] contain a 128-bit integer stored as 32-bit chunks, // with [0] holding the least significant chunk and [3] holding the most // significant, and the same for [7:4]. That is to say, from a 32-bit chunk // perspective, they are stored in baselineScratchWords_ little-endianly. // (Each chunk is itself stored in the endianness of the host; that does not // concern us here.) if (isAdd & 1) {
uint32_t carryIn = 0; for (int i = 0; i < 4; i++) {
uint32_t argL = instance->baselineScratchWords_[i + 0];
uint32_t argR = instance->baselineScratchWords_[i + 4];
uint32_t sum = argL + argR + carryIn;
instance->baselineScratchWords_[i + 0] = sum;
uint32_t carryOut = carryIn ? (sum <= argL) : (sum < argL);
carryIn = carryOut;
}
} else {
uint32_t borrowIn = 0; for (int i = 0; i < 4; i++) {
uint32_t argL = instance->baselineScratchWords_[i + 0];
uint32_t argR = instance->baselineScratchWords_[i + 4];
uint32_t diff = argL - argR - borrowIn;
instance->baselineScratchWords_[i + 0] = diff;
uint32_t borrowOut = borrowIn ? (argL <= argR) : (argL < argR);
borrowIn = borrowOut;
}
} #else // This helper should only be called on 32-bit targets.
MOZ_CRASH(); #endif// JS_64BIT
}
void Instance::mulI64Wide(Instance* instance, uint32_t isSigned) { #ifndef JS_64BIT
static_assert(sizeof(instance->baselineScratchWords_[0]) == sizeof(uint32_t));
static_assert(N_BASELINE_SCRATCH_WORDS >= 4); // Compute // // baselineScratchWords_[3:0] // = baselineScratchWords_[1:0] *widen baselineScratchWords_[3:2] // // using the same storage conventions as add128/sub128 above. // If `isSigned[0]` is 1, the operands are (conceptually) signedly-widened // before multiplication, otherwise they are unsignedly widened. This scheme // is feasible on 32-bit targets because gcc and clang both support 64-bit // arithmetic even on 32-bit targets.
uint64_t x = (uint64_t(instance->baselineScratchWords_[1]) << 32) |
uint64_t(instance->baselineScratchWords_[0]);
uint64_t y = (uint64_t(instance->baselineScratchWords_[3]) << 32) |
uint64_t(instance->baselineScratchWords_[2]);
// Compute zHi:zLo = x *widen y. First, calculate and store zLo.
uint64_t zLo = x * y;
instance->baselineScratchWords_[0] = uint32_t(zLo >> 0);
instance->baselineScratchWords_[1] = uint32_t(zLo >> 32);
// Now compute and store zHi, which is much more complex.
uint64_t temp1 = x & 0xFFFFFFFFULL;
uint64_t temp2 = y & 0xFFFFFFFFULL;
uint64_t temp3 = temp1 * temp2;
// See Henry S. Warren, Jr's "Hackers Delight", Chapter 8 (Multiplication). if (isSigned & 1) {
x = int64_t(x) >> 32;
y = int64_t(y) >> 32;
temp2 *= x;
temp1 *= y;
temp3 = uint64_t(temp3) >> 32; // yes, really an unsigned shift
x *= y;
temp3 += temp2;
temp2 = temp3 & 0xFFFFFFFFULL;
temp3 = int64_t(temp3) >> 32;
temp1 += temp2;
temp3 += x;
temp1 = int64_t(temp1) >> 32;
} else { // Exactly the same thing, except with unsigned instead of signed shifts.
x = uint64_t(x) >> 32;
y = uint64_t(y) >> 32;
temp2 *= x;
temp1 *= y;
temp3 = uint64_t(temp3) >> 32;
x *= y;
temp3 += temp2;
temp2 = temp3 & 0xFFFFFFFFULL;
temp3 = uint64_t(temp3) >> 32;
temp1 += temp2;
temp3 += x;
temp1 = uint64_t(temp1) >> 32;
}
temp3 += temp1; // temp3 now holds zHi
instance->baselineScratchWords_[2] = uint32_t(temp3 >> 0);
instance->baselineScratchWords_[3] = uint32_t(temp3 >> 32); #else // This helper should only be called on 32-bit targets.
MOZ_CRASH(); #endif// JS_64BIT
}
// [SMDOC] Wasm Function.prototype.call.bind optimization // // Check if our import is of the form `Function.prototype.call.bind(targetFunc)` // and optimize it so that we call `targetFunc` directly and pass the // first wasm function parameter as the 'this' value. // // Breaking it down: // 1. `Function.prototype.call` invokes the function given by `this` // and passes the first argument as the `this` value, then the // remaining arguments as the natural arguments. // 2. `Function.prototype.bind` creates a new bound function that will // always pass a chosen value as the `this` value. // 3. Binding 'targetFunc' to `Function.prototype.call` is equivalent to // `(thisValue, ...args) => targetFunc.call(thisValue, ...args)`; // but in a form the VM can pattern match on easily. // // When all of these conditions match, we set the `isFunctionCallBind` flag on // FuncImportInstanceData and set callable to `targetFunc`. Then // Instance::callImport reads the flag to figure out if the first parameter // should be stored in invokeArgs.thisv() or in normal arguments. // // JIT exits do not support this flag yet, and so we don't use them on the // targetFunc. This is okay because we couldn't use them on BoundFunctionObject // anyways, and so this is strictly faster. Eventually we can add JIT exit // support here.
JSObject* MaybeOptimizeFunctionCallBind(const wasm::FuncType& funcType,
JSObject* f) { // Skip this for functions with no args. This is useless as it would result // in `this` always being undefined. Skipping this simplifies the logic in // Instance::callImport. if (funcType.args().length() == 0) { return nullptr;
}
if (!f->is<BoundFunctionObject>()) { return nullptr;
}
// There cannot be any extra bound args in addition to the 'this'. if (boundFun->numBoundArgs() != 0) { return nullptr;
}
// The bound `target` must be the Function.prototype.call builtin if (!IsNativeFunction(boundTarget, fun_call)) { return nullptr;
}
// The bound `this` must be a callable object if (!boundThis.isObject() || !boundThis.toObject().isCallable() ||
IsCrossCompartmentWrapper(boundThis.toObjectOrNull())) { return nullptr;
}
// The bound `this` must not be a wasm function, or else we'll need to update // all the users of FuncImportInstanceData::callable so they don't mistake // the unwrapped import for originally being a wasm function. if (boundThis.toObject().is<JSFunction>() &&
boundThis.toObject().as<JSFunction>().isWasm()) { return nullptr;
}
return boundThis.toObjectOrNull();
}
////////////////////////////////////////////////////////////////////////////// // // Instance creation and related.
// Initialize the request-tier-up stub pointer, if relevant if (code().mode() == CompileMode::LazyTiering) {
setRequestTierUpStub(code().sharedStubs().base() +
code().requestTierUpStubOffset());
setUpdateCallRefMetricsStub(code().sharedStubs().base() +
code().updateCallRefMetricsStubOffset());
} else {
setRequestTierUpStub(nullptr);
setUpdateCallRefMetricsStub(nullptr);
}
// Initialize the hotness counters, if relevant. if (code().mode() == CompileMode::LazyTiering) { // Computing the initial hotness counters requires the code section size. const size_t codeSectionSize = codeMeta().codeSectionSize(); for (uint32_t funcIndex = codeMeta().numFuncImports;
funcIndex < codeMeta().numFuncs(); funcIndex++) {
funcDefInstanceData(funcIndex)->hotnessCounter =
computeInitialHotnessCounter(funcIndex, codeSectionSize);
}
}
// Initialize type definitions in the instance data. const SharedTypeContext& types = codeMeta().types;
Zone* zone = realm()->zone(); for (uint32_t typeIndex = 0; typeIndex < types->length(); typeIndex++) { constTypeDef& typeDef = types->type(typeIndex);
TypeDefInstanceData* typeDefData = typeDefInstanceData(typeIndex);
// Set default field values. new (typeDefData) TypeDefInstanceData();
// Store the runtime type for this type index
typeDefData->typeDef = &typeDef;
typeDefData->superTypeVector = typeDef.superTypeVector();
if (typeDef.kind() == TypeDefKind::Struct || typeDef.kind() == TypeDefKind::Array) { // Compute the parameters that allocation will use. First, the class for // the type definition. if (typeDef.kind() == TypeDefKind::Struct) { const StructType& structType = typeDef.structType(); bool needsOOLstorage = structType.hasOOL();
typeDefData->clasp =
WasmStructObject::classFromOOLness(needsOOLstorage);
} else {
typeDefData->clasp = &WasmArrayObject::class_;
}
// Find the shape using the class and recursion group const ObjectFlags objectFlags = {ObjectFlag::NotExtensible};
typeDefData->shape = WasmGCShape::getShape(
cx, typeDefData->clasp, cx->realm(), TaggedProto(),
&typeDef.recGroup(), objectFlags); if (!typeDefData->shape) { returnfalse;
}
// If `typeDef` is a struct, cache some layout info here, so that // allocators don't have to chase back through `typeDef` to determine // that. Similarly, if `typeDef` is an array, cache its array element // size here. if (typeDef.kind() == TypeDefKind::Struct) { const StructType& structType = typeDef.structType();
typeDefData->cached.strukt.payloadOffsetIL =
structType.payloadOffsetIL_;
typeDefData->cached.strukt.totalSizeIL = structType.totalSizeIL_;
typeDefData->cached.strukt.totalSizeOOL = structType.totalSizeOOL_;
typeDefData->cached.strukt.oolPointerOffset =
structType.oolPointerOffset_;
typeDefData->cached.strukt.allocKind =
gc::GetFinalizedAllocKindForClass(structType.allocKind_,
typeDefData->clasp);
MOZ_ASSERT(!IsFinalizedKind(typeDefData->cached.strukt.allocKind)); // StructLayout::totalSizeIL/OOL() ensures these are an integral number // of words.
MOZ_ASSERT(
(typeDefData->cached.strukt.totalSizeIL % sizeof(uintptr_t)) == 0);
MOZ_ASSERT(
(typeDefData->cached.strukt.totalSizeOOL % sizeof(uintptr_t)) == 0);
} else {
uint32_t arrayElemSize = typeDef.arrayType().elementType().size();
typeDefData->cached.array.elemSize = arrayElemSize;
MOZ_ASSERT(arrayElemSize == 16 || arrayElemSize == 8 ||
arrayElemSize == 4 || arrayElemSize == 2 ||
arrayElemSize == 1);
}
} elseif (typeDef.kind() == TypeDefKind::Func) { // Nothing to do; the default values are OK.
} #ifdef ENABLE_WASM_JSPI elseif (typeDef.kind() == TypeDefKind::Cont) { // Nothing to do; the default values are OK.
} #endif else {
MOZ_ASSERT(typeDef.kind() == TypeDefKind::None);
MOZ_CRASH();
}
}
// Create and initialize alloc sites, they are all the same for Wasm.
uint32_t allocSitesCount = codeTailMeta().numAllocSites; if (allocSitesCount > 0) {
mozilla::CheckedInt<size_t> numBytesRequired =
mozilla::CheckedInt<size_t>(allocSitesCount) *
mozilla::CheckedInt<size_t>(sizeof(gc::AllocSite)); if (!numBytesRequired.isValid()) {
ReportOutOfMemory(cx); returnfalse;
}
allocSites_ = (gc::AllocSite*)js_malloc(numBytesRequired.value()); if (!allocSites_) {
ReportOutOfMemory(cx); returnfalse;
} for (uint32_t i = 0; i < allocSitesCount; ++i) { new (&allocSites_[i]) gc::AllocSite();
allocSites_[i].initWasm(zone);
}
}
// Initialize function imports in the instance data for (size_t i = 0; i < code().funcImports().length(); i++) {
JSObject* f = funcImports[i];
#ifdef ENABLE_WASM_JSPI if (JSObject* suspendingObject = MaybeUnwrapSuspendingObject(f)) { // Compile suspending function Wasm wrapper.
uint32_t funcTypeIndex = codeMeta().funcs[i].typeIndex;
RootedObject wrapped(cx, suspendingObject);
RootedFunction wrapper(
cx, WasmSuspendingFunctionCreate(cx, wrapped, funcTypeIndex,
codeMeta().types)); if (!wrapper) { returnfalse;
} // The wrapper must expose exactly the import's declared type so that // ref.test/ref.cast/call_indirect against that type behave correctly.
MOZ_RELEASE_ASSERT(wrapper->isWasm());
MOZ_RELEASE_ASSERT(&wrapper->wasmInstance().codeMeta().getFuncTypeDef(
wrapper->wasmFuncIndex()) ==
&codeMeta().getFuncTypeDef(i));
f = wrapper;
} #endif
#ifdef DEBUG for (size_t i = 0; i < codeMeta().numExportedFuncs(); i++) {
MOZ_ASSERT(!funcExportInstanceData(i).func);
} #endif
// We use writeToTenuredHeapLocation below as WasmInstanceObject is always // tenured.
Rooted<WasmInstanceObject*> instanceObj(cx, object());
MOZ_ASSERT(instanceObj->isTenured());
// Initialize globals in the instance data. // // This must be performed after we have initialized runtime types as a global // initializer may reference them. // // We increment `maxInitializedGlobalsIndexPlus1_` every iteration of the // loop, as we call out to `InitExpr::evaluate` which may call // `constantGlobalGet` which uses this value to assert we're never accessing // uninitialized globals.
maxInitializedGlobalsIndexPlus1_ = 0; for (size_t i = 0; i < codeMeta().globals.length();
i++, maxInitializedGlobalsIndexPlus1_ = i) { const GlobalDesc& global = codeMeta().globals[i];
// Constants are baked into the code, never stored in the global area. if (global.isConstant()) { continue;
}
if (global.isIndirect()) { // Initialize the cell
globalObjs[i]->setVal(val);
// Link to the cell
*(void**)globalAddr = globalObjs[i]->addressOfCell();
} else {
val.get().writeToTenuredHeapLocation(globalAddr);
} break;
} case GlobalKind::Constant: {
MOZ_CRASH("skipped at the top");
}
}
}
// All globals were initialized
MOZ_ASSERT(maxInitializedGlobalsIndexPlus1_ == codeMeta().globals.length());
// Initialize memories in the instance data for (size_t i = 0; i < memories.length(); i++) { const MemoryDesc& md = codeMeta().memories[i];
MemoryInstanceData& data = memoryInstanceData(i);
WasmMemoryObject* memory = memories.get()[i];
// Initialize tables in the instance data for (size_t i = 0; i < tables_.length(); i++) { const TableDesc& td = codeMeta().tables[i];
TableInstanceData& table = tableInstanceData(i);
table.length = tables_[i]->length();
table.elements = tables_[i]->instanceElements(); // Non-imported tables, with init_expr, has to be initialized with // the evaluated value. if (!td.isImported && td.initExpr) {
Rooted<WasmInstanceObject*> instanceObj(cx, object());
RootedVal val(cx); if (!td.initExpr->evaluate(cx, instanceObj, &val)) { returnfalse;
}
RootedAnyRef ref(cx, val.get().ref());
tables_[i]->fillUninitialized(0, tables_[i]->length(), ref, cx);
}
}
#ifdef DEBUG // All (linked) tables with non-nullable types must be initialized. for (size_t i = 0; i < tables_.length(); i++) { const TableDesc& td = codeMeta().tables[i]; if (!td.elemType().isNullable()) {
tables_[i]->assertRangeNotNull(0, tables_[i]->length());
}
} #endif// DEBUG
// Initialize tags in the instance data for (size_t i = 0; i < codeMeta().tags.length(); i++) {
MOZ_ASSERT(tagObjs[i] != nullptr);
tagInstanceData(i).object = tagObjs[i];
}
pendingException_ = nullptr;
pendingExceptionTag_ = nullptr;
if (code().mode() == CompileMode::LazyTiering) {
callRefMetrics_ = (CallRefMetrics*)js_calloc(
codeTailMeta().numCallRefMetrics, sizeof(CallRefMetrics)); if (!callRefMetrics_) {
ReportOutOfMemory(cx); returnfalse;
} // A zeroed-out CallRefMetrics should satisfy // CallRefMetrics::checkInvariants.
MOZ_ASSERT_IF(codeTailMeta().numCallRefMetrics > 0,
callRefMetrics_[0].checkInvariants());
} else {
MOZ_ASSERT(codeTailMeta().numCallRefMetrics == 0);
}
// Add observers if our tables may grow for (const SharedTable& table : tables_) { if (table->movingGrowable() && !table->addMovingGrowObserver(cx, object_)) { returnfalse;
}
}
// Take references to the passive data segments if (!passiveDataSegments_.resize(dataSegments.length())) {
ReportOutOfMemory(cx); returnfalse;
} for (size_t i = 0; i < dataSegments.length(); i++) { if (!dataSegments[i]->active()) {
passiveDataSegments_[i] = dataSegments[i];
}
}
// Create InstanceElemSegments for any passive element segments, since these // are the ones available at runtime. if (!passiveElemSegments_.resize(elemSegments.length())) {
ReportOutOfMemory(cx); returnfalse;
} for (size_t i = 0; i < elemSegments.length(); i++) { const ModuleElemSegment& seg = elemSegments[i]; if (seg.kind == ModuleElemSegment::Kind::Passive) {
passiveElemSegments_[i] = InstanceElemSegment();
InstanceElemSegment& instanceSeg = passiveElemSegments_[i]; if (!instanceSeg.reserve(seg.numElements())) {
ReportOutOfMemory(cx); returnfalse;
}
// Limits as set by InliningHeuristics::InliningHeuristics(). const DebugOnly<float> epsilon = 0.000001;
MOZ_ASSERT(requiredHotnessFraction >= 0.1 - epsilon);
MOZ_ASSERT(requiredHotnessFraction <= 1.0 + epsilon);
CallRefMetricsRange range = codeTailMeta().getFuncDefCallRefs(funcIndex); for (uint32_t callRefIndex = range.begin;
callRefIndex < range.begin + range.length; callRefIndex++) {
MOZ_RELEASE_ASSERT(callRefIndex < codeTailMeta().numCallRefMetrics);
// In this loop, for each CallRefMetrics, we create a corresponding // CallRefHint. The CallRefHint is a recommendation of which function(s) // to inline into the associated call site. It is based on call target // counts at the call site and incorporates other heuristics as implemented // by the code below. // // Later, when compiling the call site with Ion, the CallRefHint created // here is consulted. That may or may not result in inlining actually // taking place, since it depends also on context known only at // Ion-compilation time -- inlining depth, inlining budgets, etc. In // particular, if the call site is itself within a function that got // inlined multiple times, the call site may be compiled multiple times, // with inlining happening in some cases and not in others. // // The logic below tries to find reasons not to inline into this call site, // and if none are found, creates and stores a CallRefHint specifying the // recommended targets. // // The core criterion is that the set of targets that eventually get chosen // must together make up at least `requiredHotnessFraction` of all calls // made by this call site.
// For convenience, work with a copy of the candidates, not directly with // `metrics`. struct Candidate {
uint32_t funcIndex = 0;
uint32_t count = 0;
Candidate() = default;
Candidate(const Candidate&) = default;
Candidate(uint32_t funcIndex, uint32_t count)
: funcIndex(funcIndex), count(count) {}
};
Candidate candidates[CallRefMetrics::NUM_SLOTS];
size_t numCandidates = 0;
// If we're going to recommend no inlining here, specify a reason. constchar* skipReason = nullptr;
// The total count for targets that are individually tracked.
uint64_t totalTrackedCount = 0; bool allCandidatesAreImports = true;
// Make a first pass over the candidates, skipping imports. for (size_t i = 0; i < CallRefMetrics::NUM_SLOTS; i++) { if (!metrics.targets[i]) { break;
}
uint32_t targetCount = metrics.counts[i]; if (targetCount == 0) { continue;
}
totalTrackedCount += uint64_t(targetCount);
// We can't inline a call to a function which is in this module but has a // different Instance, since the potential callees of any function depend // on the instance it is associated with. Cross-instance calls should // have already been excluded from consideration by the code generated by // BaseCompiler::updateCallRefMetrics, but given that this is critical, // assert it here. const DebugOnly<Instance*> targetFuncInstance = static_cast<wasm::Instance*>(
metrics.targets[i]
->getExtendedSlot(FunctionExtended::WASM_INSTANCE_SLOT)
.toPrivate());
MOZ_ASSERT(targetFuncInstance == this);
// The total count of all calls made by this call site.
uint64_t totalCount = totalTrackedCount + uint64_t(metrics.countOther);
// Throw out some obvious cases. if (totalCount == 0) { // See comments on definition of CallRefMetrics regarding overflow.
skipReason = "(callsite unused)";
} elseif (metrics.targets[0] == nullptr) { // None of the calls made by this call site could be attributed to // specific callees; they all got lumped into CallRefMetrics::countOther. // See GenerateUpdateCallRefMetricsStub for possible reasons why.
skipReason = "(no individually tracked targets)";
} elseif (numCandidates > 0 && allCandidatesAreImports) { // Imported functions can't be inlined.
skipReason = "(all targets are imports)";
}
// We want to avoid inlining large functions into cold(ish) call sites. if (!skipReason) {
uint32_t totalTargetBodySize = 0; for (size_t i = 0; i < numCandidates; i++) {
totalTargetBodySize +=
codeTailMeta().funcDefRange(candidates[i].funcIndex).size();
} if (totalCount < 2 * totalTargetBodySize) {
skipReason = "(callsite too cold)";
}
}
// The final check is the most important. We need to choose some subset of // the candidates which together make up at least `requiredHotnessFraction` // of the calls made by this call site. However, to avoid generated code // wasting time on checking guards for relatively unlikely targets, we // ignore any candidate that does not achieve at least 10% of // `requiredHotnessFraction`. Also make up a CallRefHints in anticipation // of finding a usable set of candidates.
CallRefHint hints; if (!skipReason) {
MOZ_RELEASE_ASSERT(totalCount > 0); // Be sure to avoid NaN/Inf problems float usableFraction = 0.0;
uint32_t numUsableCandidates = 0; for (size_t i = 0; i < numCandidates; i++) { float candidateFraction = float(candidates[i].count) / float(totalCount); if (candidateFraction >= 0.1 * requiredHotnessFraction) {
usableFraction += candidateFraction;
numUsableCandidates++; if (!hints.full()) { // Add this candidate to `hints`. This assumes that we // (more-or-less) encounter candidates in declining order of // hotness. See block comment on `struct CallRefMetrics`.
hints.append(candidates[i].funcIndex);
}
}
} if (numUsableCandidates == 0) {
skipReason = "(no target is hot enough)";
} elseif (usableFraction < requiredHotnessFraction) {
skipReason = "(collectively not hot enough)";
}
}
void Instance::tracePrivate(JSTracer* trc) { // This method is only called from WasmInstanceObject so the only reason why // TraceEdge is called is so that the pointer can be updated during a moving // GC.
MOZ_ASSERT_IF(trc->isMarkingTracer(), gc::IsMarked(trc->runtime(), object_));
TraceEdge(trc, &object_, "wasm instance object");
// OK to just do one tier here; though the tiers have different funcImports // tables, they share the instance object. for (uint32_t funcIndex = 0; funcIndex < codeMeta().numFuncImports;
funcIndex++) {
TraceEdge(trc, &funcImportInstanceData(funcIndex).callable, "wasm import");
}
void js::wasm::TraceInstanceEdge(JSTracer* trc, Instance* instance, constchar* name) { if (IsTracerKind(trc, JS::TracerKind::Moving)) { // Compacting GC: The Instance does not move so there is nothing to do here. // Reading the object from the instance below would be a data race during // multi-threaded updates. Compacting GC does not rely on graph traversal // to find all edges that need to be updated. return;
}
// Instance fields are traced by the owning WasmInstanceObject's trace // hook. Tracing this ensures they are traced once.
JSObject* object = instance->objectUnbarriered();
TraceManuallyBarrieredEdge(trc, &object, name);
}
static uintptr_t* GetFrameScanStartForStackMap( const Frame* frame, const StackMap* map,
uintptr_t* highestByteVisitedInPrevFrame) { // |frame| points somewhere in the middle of the area described by |map|. // We have to calculate |scanStart|, the lowest address that is described by // |map|, by consulting |map->frameOffsetFromTop|.
// Do what we can to assert that, for consecutive wasm frames, their stack // maps also abut exactly. This is a useful sanity check on the sizing of // stackmaps. // // In debug builds, the stackmap construction machinery goes to considerable // efforts to ensure that the stackmaps for consecutive frames abut exactly. // This is so as to ensure there are no areas of stack inadvertently ignored // by a stackmap, nor covered by two stackmaps. Hence any failure of this // assertion is serious and should be investigated. #ifndef JS_CODEGEN_ARM64
MOZ_ASSERT_IF(
highestByteVisitedInPrevFrame && *highestByteVisitedInPrevFrame != 0,
*highestByteVisitedInPrevFrame + 1 == scanStart); #endif
// If we have some exit stub words, this means the map also covers an area // created by a exit stub, and so the highest word of that should be a // constant created by (code created by) GenerateTrapExit.
MOZ_ASSERT_IF(map->header.numExitStubWords > 0,
((uintptr_t*)scanStart)[map->header.numExitStubWords - 1 -
TrapExitDummyValueOffsetFromTop] ==
TrapExitDummyValue);
// Hand refs off to the GC. for (uint32_t i = 0; i < map->header.numMappedWords; i++) { if (map->get(i) != StackMap::Kind::AnyRef) { continue;
}
TraceManuallyBarrieredEdge(trc, (AnyRef*)&stackWords[i], "Instance::traceWasmFrame: normal word");
}
// Deal with any GC-managed fields in the DebugFrame, if it is // present and those fields may be live. if (map->header.hasDebugFrameWithLiveRefs) {
DebugFrame* debugFrame = DebugFrame::from(frame); char* debugFrameP = (char*)debugFrame;
for (size_t i = 0; i < MaxRegisterResults; i++) { if (debugFrame->hasSpilledRegisterRefResult(i)) { char* resultRefP = debugFrameP + DebugFrame::offsetOfRegisterResult(i);
TraceManuallyBarrieredEdge(
trc, (AnyRef*)resultRefP, "Instance::traceWasmFrame: DebugFrame::resultResults_");
}
}
// Update array data pointers, both IL and OOL, and struct data pointers, // which are only OOL, for any such data areas that moved. Note, the // remapping info consulted by the calls to Nursery::forwardBufferPointer is // what previous calls to Nursery::setForwardingPointerWhileTenuring in // Wasm{Struct,Array}Object::obj_moved set up.
for (uint32_t i = 0; i < map->header.numMappedWords; i++) {
StackMap::Kind kind = map->get(i);
switch (kind) { case StackMap::Kind::ArrayDataPointer: { // The following makes more sense if you look at the pictures in the // SMDOC at the definition of WasmArrayData, and also read-along in // WasmArrayObject::obj_moved, which sets up the forwarding information // which we now will consult.
// Make oldDataPointer point at the storage array in the old object.
uint8_t* oldDataPointer = (uint8_t*)stackWords[i]; if (WasmArrayObject::isDataInline(oldDataPointer)) { // It's a pointer into the object itself. Figure out where the old // object is, ask where it got moved to, and fish out the updated // value from the new object.
WasmArrayObject* oldArray =
WasmArrayObject::fromInlineDataPointer(oldDataPointer);
WasmArrayObject* newArray =
(WasmArrayObject*)gc::MaybeForwarded(oldArray); if (newArray != oldArray) {
stackWords[i] =
uintptr_t(WasmArrayObject::addressOfInlineArrayData(newArray));
MOZ_ASSERT(WasmArrayObject::isDataInline((uint8_t*)stackWords[i]));
}
} else { // It's a pointer managed by BufferAllocator. The forwarded location // is stored in the OOLHeader::word field of the old block, with its // bit zero set to 1.
WasmArrayObject::OOLDataHeader* oldHeader =
WasmArrayObject::oolDataHeaderFromDataPointer(oldDataPointer); if (nursery.isInside((constvoid*)oldHeader)) { // If the old header word is OOLDataHeader_Magic it means there's // no forwarding pointer stored there, so don't update the stack // slot. if (oldHeader->word != WasmArrayObject::OOLDataHeader_Magic) {
MOZ_ASSERT(oldHeader->word & 1);
WasmArrayObject::OOLDataHeader* newHeader =
(WasmArrayObject::OOLDataHeader*)(oldHeader->word &
~uintptr_t(1));
MOZ_ASSERT(newHeader != oldHeader);
stackWords[i] = uintptr_t(
WasmArrayObject::oolDataHeaderToDataPointer(newHeader));
newHeader->word = WasmArrayObject::OOLDataHeader_Magic;
}
}
} break;
}
case StackMap::Kind::StructDataPointer: { // It's an unmodified pointer from BufferAllocator, so this is simple.
nursery.forwardBufferPointer(&stackWords[i]); break;
}
#ifdef DEBUG // EnsureEntryStubs() has ensured proper jit-entry stubs have been created and // installed in funcIndex's JumpTable entry, so check against the presence of // the provisional lazy stub. See also // WasmInstanceObject::getExportedFunction(). if (!funcExport->hasEagerStubs() && (*funcType)->canHaveJitEntry()) { if (!EnsureBuiltinThunksInitialized()) {
ReportOutOfMemory(cx); returnfalse;
}
JSFunction& callee = args.callee().as<JSFunction>(); void* provisionalLazyJitEntryStub = ProvisionalLazyJitEntryStub();
MOZ_ASSERT(provisionalLazyJitEntryStub);
MOZ_ASSERT(callee.isWasmWithJitEntry());
MOZ_ASSERT(*callee.wasmJitEntry() != provisionalLazyJitEntryStub);
} #endif returntrue;
}
bool wasm::ResultsToJSValue(JSContext* cx, ResultType type, void* registerResultLoc,
Maybe<char*> stackResultsLoc,
MutableHandleValue rval, CoercionLevel level) { if (type.empty()) { // No results: set to undefined, and we're done.
rval.setUndefined(); returntrue;
}
// If we added support for multiple register results, we'd need to establish a // convention for how to store them to memory in registerResultLoc. For now // we can punt.
static_assert(MaxRegisterResults == 1);
// Stack results written to stackResultsLoc; register result written // to registerResultLoc.
// First, convert the register return value, and prepare to iterate in // push order. Note that if the register result is a reference type, // it may be unrooted, so ToJSValue_anyref must not GC in that case.
ABIResultIter iter(type);
DebugOnly<bool> usedRegisterResult = false; for (; !iter.done(); iter.next()) { if (iter.cur().inRegister()) {
MOZ_ASSERT(!usedRegisterResult); if (!ToJSValue<DebugCodegenVal>(cx, registerResultLoc, iter.cur().type(),
rval, level)) { returnfalse;
}
usedRegisterResult = true;
}
}
MOZ_ASSERT(usedRegisterResult);
MOZ_ASSERT((stackResultsLoc.isSome()) == (iter.count() > 1)); if (!stackResultsLoc) { // A single result: we're done. returntrue;
}
// Otherwise, collect results in an array, in push order.
Rooted<ArrayObject*> array(cx, NewDenseEmptyArray(cx)); if (!array) { returnfalse;
}
RootedValue tmp(cx); for (iter.switchToPrev(); !iter.done(); iter.prev()) { const ABIResult& result = iter.cur(); if (result.onStack()) { char* loc = stackResultsLoc.value() + result.stackOffset(); if (!ToJSValue<DebugCodegenVal>(cx, loc, result.type(), &tmp, level)) { returnfalse;
} if (!NewbornArrayPush(cx, array, tmp)) { returnfalse;
}
} else { if (!NewbornArrayPush(cx, array, rval)) { returnfalse;
}
}
}
rval.set(ObjectValue(*array)); returntrue;
}
class MOZ_RAII ReturnToJSResultCollector { class MOZ_RAII StackResultsRooter : public JS::CustomAutoRooter {
ReturnToJSResultCollector& collector_;
public: explicit ReturnToJSResultCollector(const ResultType& type) : type_(type) {}; bool init(JSContext* cx) { bool needRooter = false;
ABIResultIter iter(type_); for (; !iter.done(); iter.next()) { const ABIResult& result = iter.cur(); if (result.onStack() && result.type().isRefRepr()) {
needRooter = true;
}
}
uint32_t areaBytes = iter.stackBytesConsumedSoFar();
MOZ_ASSERT_IF(needRooter, areaBytes > 0); if (areaBytes > 0) { // It is necessary to zero storage for ref results, and it doesn't // hurt to do so for other POD results.
stackResultsArea_ = cx->make_zeroed_pod_array<char>(areaBytes); if (!stackResultsArea_) { returnfalse;
} if (needRooter) {
rooter_.emplace(cx, *this);
}
} returntrue;
}
// Early exit if we've already found or created this exported function if (instanceData.func) {
result.set(instanceData.func); returntrue;
}
// If this is an import, we need to recover the original function to maintain // reference equality between a re-exported function and 'ref.func'. The // identity of the imported function object is stable across tiers, which is // what we want. // // Use the imported function only if it is an exported function, otherwise // fall through to get a (possibly new) exported function. if (funcIndex < codeMeta().numFuncImports) {
FuncImportInstanceData& import = funcImportInstanceData(funcIndex); if (import.callable->is<JSFunction>()) {
JSFunction* fun = &import.callable->as<JSFunction>(); if (!codeMeta().funcImportsAreJS && fun->isWasm()) { // Unwrapped Function.prototype.call.bind imports should not be used // when the unwrapped function is a wasm function.
MOZ_ASSERT(!import.isFunctionCallBind);
instanceData.func = fun;
result.set(fun); returntrue;
}
}
}
// Otherwise this is a locally defined function which we've never created a // function object for yet. const CodeBlock& codeBlock = code().funcCodeBlock(funcIndex); const CodeRange& codeRange = codeBlock.codeRange(funcIndex); constTypeDef& funcTypeDef = codeMeta().getFuncTypeDef(funcIndex); unsigned numArgs = funcTypeDef.funcType().args().length();
Instance* instance = const_cast<Instance*>(this); const SuperTypeVector* superTypeVector = funcTypeDef.superTypeVector(); void* uncheckedCallEntry =
codeBlock.base() + codeRange.funcUncheckedCallEntry();
if (isAsmJS()) { // asm.js needs to act like a normal JS function which means having the // name from the original source and being callable as a constructor.
Rooted<JSAtom*> name(cx, getFuncDisplayAtom(cx, funcIndex)); if (!name) { returnfalse;
}
result.set(NewNativeConstructor(cx, WasmCall, numArgs, name,
gc::AllocKind::FUNCTION_EXTENDED,
TenuredObject, FunctionFlags::ASMJS_CTOR)); if (!result) { returnfalse;
}
MOZ_ASSERT(result->isTenured());
STATIC_ASSERT_WASM_FUNCTIONS_TENURED;
// asm.js does not support jit entries.
result->initWasm(funcIndex, instance, superTypeVector, uncheckedCallEntry);
} else {
Rooted<JSAtom*> name(cx, NumberToAtom(cx, funcIndex)); if (!name) { returnfalse;
}
RootedObject proto(cx); #ifdef ENABLE_WASM_TYPE_REFLECTIONS
proto = GlobalObject::getOrCreatePrototype(cx, JSProto_WasmFunction); if (!proto) { returnfalse;
} #endif
result.set(NewFunctionWithProto(
cx, WasmCall, numArgs, FunctionFlags::WASM, nullptr, name, proto,
gc::AllocKind::FUNCTION_EXTENDED, TenuredObject)); if (!result) { returnfalse;
}
MOZ_ASSERT(result->isTenured());
STATIC_ASSERT_WASM_FUNCTIONS_TENURED;
// Some applications eagerly access all table elements which currently // triggers worst-case behavior for lazy stubs, since each will allocate a // separate 4kb code page. Most eagerly-accessed functions are not called, // so use a shared, provisional (and slow) lazy stub as JitEntry and wait // until Instance::callExport() to create the fast entry stubs. if (funcTypeDef.funcType().canHaveJitEntry()) { const FuncExport& funcExport = codeBlock.lookupFuncExport(funcIndex); if (!funcExport.hasEagerStubs()) { if (!EnsureBuiltinThunksInitialized()) { returnfalse;
} void* provisionalLazyJitEntryStub = ProvisionalLazyJitEntryStub();
MOZ_ASSERT(provisionalLazyJitEntryStub);
code().setJitEntryIfNull(funcIndex, provisionalLazyJitEntryStub);
}
result->initWasmWithJitEntry(code().getAddressOfJitEntry(funcIndex),
instance, superTypeVector,
uncheckedCallEntry);
} else {
result->initWasm(funcIndex, instance, superTypeVector,
uncheckedCallEntry);
}
}
instanceData.func = result; returntrue;
}
bool Instance::callExport(JSContext* cx, uint32_t funcIndex, const CallArgs& args, CoercionLevel level) { if (memory0Base_) { // If there has been a moving grow, this Instance should have been notified.
MOZ_RELEASE_ASSERT(memoryBase(0).unwrap() == memory0Base_);
}
// Lossless coercions can handle unexposable arguments or returns. This is // only available in testing code. if (level != CoercionLevel::Lossless && funcType->hasUnexposableArgOrRet()) {
JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr,
JSMSG_WASM_BAD_VAL_TYPE); returnfalse;
}
// The calling convention for an external call into wasm is to pass an // array of 16-byte values where each value contains either a coerced int32 // (in the low word), or a double value (in the low dword) value, with the // coercions specified by the wasm signature. The external entry point // unpacks this array into the system-ABI-specified registers and stack // memory and then calls into the internal entry point. The return value is // stored in the first element of the array (which, therefore, must have // length >= 1).
Vector<ExportArg, 8> exportArgs(cx); if (!exportArgs.resize(
std::max<size_t>(1, argTypes.lengthWithStackResults()))) { returnfalse;
}
DebugCodegen(DebugChannel::Function, "wasm-function[%d] arguments [",
funcIndex);
RootedValue v(cx); for (size_t i = 0; i < argTypes.lengthWithStackResults(); ++i) { void* rawArgLoc = &exportArgs[i]; if (argTypes.isSyntheticStackResultPointerArg(i)) {
*reinterpret_cast<void**>(rawArgLoc) = results.stackResultsArea(); continue;
}
size_t naturalIdx = argTypes.naturalIndex(i);
v = naturalIdx < args.length() ? args[naturalIdx] : UndefinedValue();
ValType type = funcType->arg(naturalIdx); if (!ToWebAssemblyValue<DebugCodegenVal>(cx, v, type, rawArgLoc, true,
level)) { returnfalse;
} if (type.isRefRepr()) { void* ptr = *reinterpret_cast<void**>(rawArgLoc); // Store in rooted array until no more GC is possible.
RootedAnyRef ref(cx, AnyRef::fromCompiledCode(ptr)); if (!refs.emplaceBack(ref.get())) { returnfalse;
}
DebugCodegen(DebugChannel::Function, "/(#%d)", int(refs.length() - 1));
}
}
// Copy over reference values from the rooted array, if any. if (refs.length() > 0) {
DebugCodegen(DebugChannel::Function, "; ");
size_t nextRef = 0; for (size_t i = 0; i < argTypes.lengthWithStackResults(); ++i) { if (argTypes.isSyntheticStackResultPointerArg(i)) { continue;
}
size_t naturalIdx = argTypes.naturalIndex(i);
ValType type = funcType->arg(naturalIdx); if (type.isRefRepr()) {
AnyRef* rawArgLoc = (AnyRef*)&exportArgs[i];
*rawArgLoc = refs[nextRef++];
DebugCodegen(DebugChannel::Function, " ref(#%d) := %p ", int(nextRef - 1), *(void**)rawArgLoc);
}
}
refs.clear();
}
DebugCodegen(DebugChannel::Function, "]\n");
// Ensure pending exception is cleared before and after (below) call.
MOZ_ASSERT(pendingException_.isNull());
{
JitActivation activation(cx);
// Call the per-exported-function trampoline created by GenerateEntry. auto funcPtr = JS_DATA_TO_FUNC_PTR(ExportFuncPtr, interpEntry); if (!CALL_GENERATED_2(funcPtr, exportArgs.begin(), this)) { returnfalse;
}
}
MOZ_ASSERT(pendingException_.isNull());
if (isAsmJS() && args.isConstructing()) { // By spec, when a JS function is called as a constructor and this // function returns a primary type, which is the case for all asm.js // exported functions, the returned value is discarded and an empty // object is returned instead.
PlainObject* obj = NewPlainObject(cx); if (!obj) { returnfalse;
}
args.rval().set(ObjectValue(*obj)); returntrue;
}
// Note that we're not rooting the register result, if any; we depend // on ResultsCollector::collect to root the value on our behalf, // before causing any GC. void* registerResultLoc = &exportArgs[0];
DebugCodegen(DebugChannel::Function, "wasm-function[%d]; results [",
funcIndex); if (!results.collect(cx, registerResultLoc, args.rval(), level)) { returnfalse;
}
DebugCodegen(DebugChannel::Function, "]\n");
// Constant globals are baked into the code and never stored in global data. if (global.isConstant()) { // We can just re-evaluate the global initializer to get the value.
result.set(Val(global.constantValue())); return;
}
// Otherwise, we need to load the initialized value from its cell. constvoid* cell = addressOfGlobalCell(global);
result.address()->initFromHeapLocation(global.type(), cell);
}
WasmStructObject* Instance::constantStructNewDefault(JSContext* cx,
uint32_t typeIndex) { // We assume that constant structs will have a long lifetime and hence // allocate them directly in the tenured heap. Also, we have to dynamically // decide whether an OOL storage area is required. This is slow(er); do not // call here from generated code.
TypeDefInstanceData* typeDefData = typeDefInstanceData(typeIndex); const wasm::TypeDef* typeDef = typeDefData->typeDef;
MOZ_ASSERT(typeDef->kind() == wasm::TypeDefKind::Struct);
WasmArrayObject* Instance::constantArrayNewDefault(JSContext* cx,
uint32_t typeIndex,
uint32_t numElements) {
TypeDefInstanceData* typeDefData = typeDefInstanceData(typeIndex); // We assume that constant arrays will have a long lifetime and hence // allocate them directly in the tenured heap. return WasmArrayObject::createArray<true>(cx, typeDefData, nullptr,
gc::Heap::Tenured, numElements);
}
JSAtom* Instance::getFuncDisplayAtom(JSContext* cx, uint32_t funcIndex) const { // The "display name" of a function is primarily shown in Error.stack which // also includes location, so use getFuncNameBeforeLocation.
UTF8Bytes name; bool ok; if (codeMetaForAsmJS()) {
ok = codeMetaForAsmJS()->getFuncNameForAsmJS(funcIndex, &name);
} else {
ok = codeMeta().getFuncNameForWasm(NameContext::BeforeLocation, funcIndex,
codeTailMeta().nameSectionPayload.get(),
&name);
} if (!ok) { return nullptr;
}
// `table` has grown and we must update cached data for it. Importantly, // we can have cached those data in more than one location: we'll have // cached them once for each time the table was imported into this instance. // // When an instance is registered as an observer of a table it is only // registered once, regardless of how many times the table was imported. // Thus when a table is grown, onMovingGrowTable() is only invoked once for // the table. // // Ergo we must go through the entire list of tables in the instance here // and check for the table in all the cached-data slots; we can't exit after // the first hit.
for (uint32_t i = 0; i < tables_.length(); i++) { if (tables_[i] != table) { continue;
}
TableInstanceData& table = tableInstanceData(i);
table.length = tables_[i]->length();
table.elements = tables_[i]->instanceElements();
}
}
JSString* Instance::createDisplayURL(JSContext* cx) { // In the best case, we simply have a URL, from a streaming compilation of a // fetched Response.
// Otherwise, build wasm module URL from following parts: // - "wasm:" as protocol; // - URI encoded filename from metadata (if can be encoded), plus ":"; // - 64-bit hash of the module bytes (as hex dump).
JSStringBuilder result(cx); if (!result.append("wasm:")) { return nullptr;
}
if (constchar* filename = codeMeta().scriptedCaller().source.get()) { // EncodeURI returns false due to invalid chars or OOM -- fail only // during OOM.
JSString* filenamePrefix = EncodeURI(cx, filename, strlen(filename)); if (!filenamePrefix) { if (cx->isThrowingOutOfMemory()) { return nullptr;
}
// Mark the exception as thrown from a trap to prevent if from being handled // by wasm exception handlers.
MarkPendingExceptionAsTrap(cx);
}
Messung V0.5 in Prozent
¤ Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.0.94Bemerkung:
(vorverarbeitet am 2026-09-30)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.