Commit 921135c7 authored by Thibaud Michaud's avatar Thibaud Michaud Committed by V8 LUCI CQ

[wasm] Handle arguments in stack-switching export wrapper

Use the existing generic js-to-wasm wrapper to handle arguments in
the stack-switching export wrapper, by combining them into a single
helper function parameterized by a boolean.

If the stack_switch parameter is false, the generated js-to-wasm wrapper
is the same as before.

If the stack_switch parameter is true, we allocate and switch to the new
stack before starting to process the parameters. To load the parameters,
we also keep a pointer to the old stack.
After the call, we convert the return value according to the return type
as usual, and then switch back to the parent stack (which may be
different than the original stack, but has a compatible stack frame
layout).
If the stack suspends during the call, control-flow jumps right before
we deconstruct and leave the frame, and returns the Promise as an
externref in the return register.

R=ahaas@chromium.org,jkummerow@chromium.org
CC=fgm@chromium.org

Bug: v8:12191
Change-Id: If3f8eaba8edebe6e98d4738f79f895fdb5322adc
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3460410Reviewed-by: 's avatarJakob Kummerow <jkummerow@chromium.org>
Reviewed-by: 's avatarAndreas Haas <ahaas@chromium.org>
Commit-Queue: Thibaud Michaud <thibaudm@chromium.org>
Cr-Commit-Position: refs/heads/main@{#79148}
parent 46c7768b
......@@ -2982,37 +2982,9 @@ void LoadTargetJumpBuffer(MacroAssembler* masm, Register target_continuation) {
LoadJumpBuffer(masm, target_jmpbuf, false);
}
void LoadAndCallTargetWasmFunction(MacroAssembler* masm, Register function_data,
Register wasm_instance, Register tmp1) {
// TODO(thibaudm): Handle arguments.
// Set thread_in_wasm_flag.
Register thread_in_wasm_flag_addr = tmp1;
__ movq(
thread_in_wasm_flag_addr,
MemOperand(kRootRegister, Isolate::thread_in_wasm_flag_address_offset()));
__ movl(MemOperand(thread_in_wasm_flag_addr, 0), Immediate(1));
Register function_entry = function_data;
__ LoadAnyTaggedField(
function_entry,
FieldOperand(function_entry, WasmExportedFunctionData::kInternalOffset));
__ LoadExternalPointerField(
function_entry,
FieldOperand(function_data, WasmInternalFunction::kForeignAddressOffset),
kForeignForeignAddressTag, kScratchRegister);
__ Push(wasm_instance);
__ call(function_entry);
// Note: we might be returning to an "OnFulfilled" frame if the stack was
// suspended and resumed via the chained promise.
__ Pop(wasm_instance);
// Unset thread_in_wasm_flag.
__ movq(
thread_in_wasm_flag_addr,
MemOperand(kRootRegister, Isolate::thread_in_wasm_flag_address_offset()));
__ movl(MemOperand(thread_in_wasm_flag_addr, 0), Immediate(0));
}
void ReloadParentContinuation(MacroAssembler* masm, Register wasm_instance,
Register tmp1, Register tmp2) {
Register return_reg, Register tmp1,
Register tmp2) {
Register active_continuation = tmp1;
__ LoadRoot(active_continuation, RootIndex::kActiveContinuation);
......@@ -3049,10 +3021,12 @@ void ReloadParentContinuation(MacroAssembler* masm, Register wasm_instance,
MemOperand GCScanSlotPlace =
MemOperand(rbp, BuiltinWasmWrapperConstants::kGCScanSlotCountOffset);
__ Move(GCScanSlotPlace, 1);
__ Push(return_reg);
__ Push(wasm_instance); // Spill.
__ Move(kContextRegister, Smi::zero());
__ CallRuntime(Runtime::kWasmSyncStackLimit);
__ Pop(wasm_instance);
__ Pop(return_reg);
}
void RestoreParentSuspender(MacroAssembler* masm) {
......@@ -3126,11 +3100,11 @@ void LoadValueTypesArray(MacroAssembler* masm, Register function_data,
__ movq(valuetypes_array_ptr,
MemOperand(signature, wasm::FunctionSig::kRepsOffset));
}
} // namespace
void Builtins::Generate_GenericJSToWasmWrapper(MacroAssembler* masm) {
void GenericJSToWasmWrapperHelper(MacroAssembler* masm, bool stack_switch) {
// Set up the stackframe.
__ EnterFrame(StackFrame::JS_TO_WASM);
__ EnterFrame(stack_switch ? StackFrame::STACK_SWITCH
: StackFrame::JS_TO_WASM);
// -------------------------------------------
// Compute offsets and prepare for GC.
......@@ -3139,9 +3113,10 @@ void Builtins::Generate_GenericJSToWasmWrapper(MacroAssembler* masm) {
BuiltinWasmWrapperConstants::kGCScanSlotCountOffset;
// The number of parameters passed to this function.
constexpr int kInParamCountOffset =
kGCScanSlotCountOffset - kSystemPointerSize;
BuiltinWasmWrapperConstants::kInParamCountOffset;
// The number of parameters according to the signature.
constexpr int kParamCountOffset = kInParamCountOffset - kSystemPointerSize;
constexpr int kParamCountOffset =
BuiltinWasmWrapperConstants::kParamCountOffset;
constexpr int kReturnCountOffset = kParamCountOffset - kSystemPointerSize;
constexpr int kValueTypesArrayStartOffset =
kReturnCountOffset - kSystemPointerSize;
......@@ -3163,29 +3138,87 @@ void Builtins::Generate_GenericJSToWasmWrapper(MacroAssembler* masm) {
__ movq(MemOperand(rbp, kInParamCountOffset), in_param_count);
in_param_count = no_reg;
// Initialize the {HasRefTypes} slot.
__ movq(MemOperand(rbp, kHasRefTypesOffset), Immediate(0));
Register function_data = rdi;
Register wasm_instance = rsi;
LoadFunctionDataAndWasmInstance(masm, function_data, wasm_instance);
// -------------------------------------------
// Decrement the budget of the generic wrapper in function data.
// -------------------------------------------
__ SmiAddConstant(
MemOperand(function_data, WasmExportedFunctionData::kWrapperBudgetOffset -
kHeapObjectTag),
Smi::FromInt(-1));
// -------------------------------------------
// Check if the budget of the generic wrapper reached 0 (zero).
// -------------------------------------------
// Instead of a specific comparison, we can directly use the flags set
// from the previous addition.
Label compile_wrapper, compile_wrapper_done;
__ j(less_equal, &compile_wrapper);
__ bind(&compile_wrapper_done);
if (!stack_switch) {
// -------------------------------------------
// Decrement the budget of the generic wrapper in function data.
// -------------------------------------------
__ SmiAddConstant(
MemOperand(
function_data,
WasmExportedFunctionData::kWrapperBudgetOffset - kHeapObjectTag),
Smi::FromInt(-1));
// -------------------------------------------
// Check if the budget of the generic wrapper reached 0 (zero).
// -------------------------------------------
// Instead of a specific comparison, we can directly use the flags set
// from the previous addition.
__ j(less_equal, &compile_wrapper);
__ bind(&compile_wrapper_done);
}
Label suspend;
if (stack_switch) {
Register active_continuation = rbx;
__ LoadRoot(active_continuation, RootIndex::kActiveContinuation);
SaveState(masm, active_continuation, rcx, &suspend);
AllocateContinuation(masm, function_data, wasm_instance);
Register target_continuation = rax; /* fixed */
// Save the old stack's rbp in r9, and use it to access the parameters in
// the parent frame.
// We also distribute the spill slots across the two stacks as needed by
// creating a "shadow frame":
//
// old stack: new stack:
// +-----------------+
// | <parent frame> |
// r9-> +-----------------+ +-----------------+
// | <fixed> | | 0 (jmpbuf rbp) |
// +-----------------+ rbp-> +-----------------+
// |kGCScanSlotCount | |kGCScanSlotCount |
// +-----------------+ +-----------------+
// | kParamCount | | / |
// +-----------------+ +-----------------+
// | kInParamCount | | / |
// +-----------------+ +-----------------+
// | / | | kReturnCount |
// +-----------------+ +-----------------+
// | / | |kValueTypesArray |
// +-----------------+ +-----------------+
// | / | | kHasRefTypes |
// +-----------------+ +-----------------+
// | / | | kFunctionData |
// +-----------------+ rsp-> +-----------------+
// seal stack |
// V
//
// - When we first enter the prompt, we have access to both frames, so it
// does not matter where the values are spilled.
// - When we suspend for the first time, we longjmp to the original frame
// (left). So the frame needs to contain the necessary information to
// properly deconstruct itself (actual param count and signature param
// count).
// - When we suspend for the second time, we longjmp to the frame that was
// set up by the WasmResume builtin, which has the same layout as the
// original frame (left).
// - When the closure finally resolves, we use the value types pointer
// stored in the shadow frame to get the return type and convert the return
// value accordingly.
__ movq(r9, rbp);
LoadTargetJumpBuffer(masm, target_continuation);
// Push the loaded rbp. We know it is null, because there is no frame yet,
// so we could also push 0 directly. In any case we need to push it, because
// this marks the base of the stack segment for the stack frame iterator.
__ pushq(rbp);
__ movq(rbp, rsp);
__ addq(rsp, Immediate(kLastSpillOffset));
}
Register original_fp = stack_switch ? r9 : rbp;
// -------------------------------------------
// Load values from the signature.
......@@ -3196,6 +3229,9 @@ void Builtins::Generate_GenericJSToWasmWrapper(MacroAssembler* masm) {
LoadValueTypesArray(masm, function_data, valuetypes_array_ptr, return_count,
param_count);
// Initialize the {HasRefTypes} slot.
__ movq(MemOperand(rbp, kHasRefTypesOffset), Immediate(0));
// -------------------------------------------
// Store signature-related values to the stack.
// -------------------------------------------
......@@ -3203,7 +3239,7 @@ void Builtins::Generate_GenericJSToWasmWrapper(MacroAssembler* masm) {
// We cannot push values onto the stack right before the wasm call. The wasm
// function expects the parameters, that didn't fit into the registers, on the
// top of the stack.
__ movq(MemOperand(rbp, kParamCountOffset), param_count);
__ movq(MemOperand(original_fp, kParamCountOffset), param_count);
__ movq(MemOperand(rbp, kReturnCountOffset), return_count);
__ movq(MemOperand(rbp, kValueTypesArrayStartOffset), valuetypes_array_ptr);
......@@ -3273,7 +3309,7 @@ void Builtins::Generate_GenericJSToWasmWrapper(MacroAssembler* masm) {
__ subq(rsp, params_size);
params_size = no_reg;
param_count = rcx;
__ movq(param_count, MemOperand(rbp, kParamCountOffset));
__ movq(param_count, MemOperand(original_fp, kParamCountOffset));
// -------------------------------------------
// Set up for the param evaluation loop.
......@@ -3328,7 +3364,7 @@ void Builtins::Generate_GenericJSToWasmWrapper(MacroAssembler* masm) {
Label loop_through_params;
__ bind(&loop_through_params);
__ movq(param, MemOperand(rbp, current_param, times_1, 0));
__ movq(param, MemOperand(original_fp, current_param, times_1, 0));
__ movl(valuetype,
Operand(valuetypes_array_ptr, wasm::ValueType::bit_field_offset()));
......@@ -3392,7 +3428,7 @@ void Builtins::Generate_GenericJSToWasmWrapper(MacroAssembler* masm) {
__ bind(&ref_loop_through_params);
// Load the current parameter with type.
__ movq(param, MemOperand(rbp, current_param, times_1, 0));
__ movq(param, MemOperand(original_fp, current_param, times_1, 0));
__ movl(valuetype,
Operand(valuetypes_array_ptr, wasm::ValueType::bit_field_offset()));
// Extract the ValueKind of the type, to check for kRef and kOptRef.
......@@ -3440,7 +3476,7 @@ void Builtins::Generate_GenericJSToWasmWrapper(MacroAssembler* masm) {
// -----------------------------------
Register temp_params_size = rax;
__ movq(temp_params_size, MemOperand(rbp, kParamCountOffset));
__ movq(temp_params_size, MemOperand(original_fp, kParamCountOffset));
__ shlq(temp_params_size, Immediate(kSystemPointerSizeLog2));
// We want to use the register of the function_data = rdi.
__ movq(MemOperand(rbp, kFunctionDataOffset), function_data);
......@@ -3592,6 +3628,9 @@ void Builtins::Generate_GenericJSToWasmWrapper(MacroAssembler* masm) {
// Call the Wasm function.
// -------------------------------------------
__ call(function_entry);
// Note: we might be returning to a different frame if the stack was suspended
// and resumed during the call. The new frame is set up by WasmResume and has
// a compatible layout.
function_entry = no_reg;
// -------------------------------------------
......@@ -3625,6 +3664,14 @@ void Builtins::Generate_GenericJSToWasmWrapper(MacroAssembler* masm) {
Label return_done;
__ bind(&return_done);
if (stack_switch) {
ReloadParentContinuation(masm, wasm_instance, return_reg, rbx, rcx);
RestoreParentSuspender(masm);
}
__ bind(&suspend);
// No need to process the return value if the stack is suspended, there is a
// single 'externref' value (the promise) which doesn't require conversion.
__ movq(param_count, MemOperand(rbp, kParamCountOffset));
// Calculate the number of parameters we have to pop off the stack. This
......@@ -3637,7 +3684,8 @@ void Builtins::Generate_GenericJSToWasmWrapper(MacroAssembler* masm) {
// -------------------------------------------
// Deconstrunct the stack frame.
// -------------------------------------------
__ LeaveFrame(StackFrame::JS_TO_WASM);
__ LeaveFrame(stack_switch ? StackFrame::STACK_SWITCH
: StackFrame::JS_TO_WASM);
// We have to remove the caller frame slots:
// - JS arguments
......@@ -3843,99 +3891,40 @@ void Builtins::Generate_GenericJSToWasmWrapper(MacroAssembler* masm) {
RelocInfo::CODE_TARGET);
__ jmp(&return_done);
// -------------------------------------------
// Kick off compilation.
// -------------------------------------------
__ bind(&compile_wrapper);
// Enable GC.
MemOperand GCScanSlotPlace = MemOperand(rbp, kGCScanSlotCountOffset);
__ Move(GCScanSlotPlace, 4);
// Save registers to the stack.
__ pushq(wasm_instance);
__ pushq(function_data);
// Push the arguments for the runtime call.
__ Push(wasm_instance); // first argument
__ Push(function_data); // second argument
// Set up context.
__ Move(kContextRegister, Smi::zero());
// Call the runtime function that kicks off compilation.
__ CallRuntime(Runtime::kWasmCompileWrapper, 2);
// Pop the result.
__ movq(r9, kReturnRegister0);
// Restore registers from the stack.
__ popq(function_data);
__ popq(wasm_instance);
__ jmp(&compile_wrapper_done);
if (!stack_switch) {
// -------------------------------------------
// Kick off compilation.
// -------------------------------------------
__ bind(&compile_wrapper);
// Enable GC.
MemOperand GCScanSlotPlace = MemOperand(rbp, kGCScanSlotCountOffset);
__ Move(GCScanSlotPlace, 4);
// Save registers to the stack.
__ pushq(wasm_instance);
__ pushq(function_data);
// Push the arguments for the runtime call.
__ Push(wasm_instance); // first argument
__ Push(function_data); // second argument
// Set up context.
__ Move(kContextRegister, Smi::zero());
// Call the runtime function that kicks off compilation.
__ CallRuntime(Runtime::kWasmCompileWrapper, 2);
// Pop the result.
__ movq(r9, kReturnRegister0);
// Restore registers from the stack.
__ popq(function_data);
__ popq(wasm_instance);
__ jmp(&compile_wrapper_done);
}
}
} // namespace
namespace {
// Helper function for wasm stack switching.
// Returns the wasm instance in rsi and the function data in the same register
// the input.
void GetInstanceAndFunctionData(MacroAssembler* masm, Register closure) {
Register sfi = closure;
__ LoadAnyTaggedField(
sfi,
MemOperand(
closure,
wasm::ObjectAccess::SharedFunctionInfoOffsetInTaggedJSFunction()));
Register function_data = sfi;
__ LoadAnyTaggedField(
function_data,
FieldOperand(sfi, SharedFunctionInfo::kFunctionDataOffset));
Register wasm_instance = kWasmInstanceRegister; // rsi
__ LoadAnyTaggedField(
wasm_instance,
FieldOperand(function_data, WasmExportedFunctionData::kInstanceOffset));
void Builtins::Generate_GenericJSToWasmWrapper(MacroAssembler* masm) {
GenericJSToWasmWrapperHelper(masm, false);
}
} // namespace
void Builtins::Generate_WasmReturnPromiseOnSuspend(MacroAssembler* masm) {
__ EnterFrame(StackFrame::STACK_SWITCH);
// Parameters.
Register param_count = kJavaScriptCallArgCountRegister; // rax
__ decq(param_count);
__ subq(rsp, Immediate(ReturnPromiseOnSuspendFrameConstants::kSpillAreaSize));
__ movq(
MemOperand(rbp, ReturnPromiseOnSuspendFrameConstants::kParamCountOffset),
param_count);
GetInstanceAndFunctionData(masm, kJSFunctionRegister);
Register function_data = rdi;
Register wasm_instance = kWasmInstanceRegister;
// live: [rdi, rsi]
Register active_continuation = rax;
__ LoadRoot(active_continuation, RootIndex::kActiveContinuation);
// live: [rdi, rsi, rax]
Label suspend;
SaveState(masm, active_continuation, rbx, &suspend);
// live: [rdi, rsi]
AllocateContinuation(masm, function_data, wasm_instance);
Register target_continuation = rax;
// live: [rdi, rsi, rax]
LoadTargetJumpBuffer(masm, target_continuation);
// live: [rdi, rsi]
LoadAndCallTargetWasmFunction(masm, function_data, wasm_instance, rax);
// live: [rsi]
ReloadParentContinuation(masm, wasm_instance, rax, rbx);
// live: []
RestoreParentSuspender(masm);
__ bind(&suspend);
// -------------------------------------------
// Epilogue.
// -------------------------------------------
param_count = rbx;
__ movq(
param_count,
MemOperand(rbp, ReturnPromiseOnSuspendFrameConstants::kParamCountOffset));
__ LeaveFrame(StackFrame::STACK_SWITCH);
__ DropArguments(param_count, r8, TurboAssembler::kCountIsInteger,
TurboAssembler::kCountExcludesReceiver);
__ ret(0);
GenericJSToWasmWrapperHelper(masm, true);
}
void Builtins::Generate_WasmSuspend(MacroAssembler* masm) {
......@@ -4042,14 +4031,18 @@ void Builtins::Generate_WasmResume(MacroAssembler* masm) {
Register param_count = rax;
Register closure = kJSFunctionRegister; // rdi
__ subq(rsp, Immediate(ReturnPromiseOnSuspendFrameConstants::kSpillAreaSize));
// This slot is not used in this builtin. But when we return from the resumed
// continuation, we return to the ReturnPromiseOnSuspend code, which expects
// this slot to be set.
__ movq(
MemOperand(rbp, ReturnPromiseOnSuspendFrameConstants::kParamCountOffset),
param_count);
// These slots are not used in this builtin. But when we return from the
// resumed continuation, we return to the GenericJSToWasmWrapper code, which
// expects these slots to be set.
constexpr int kInParamCountOffset =
BuiltinWasmWrapperConstants::kInParamCountOffset;
constexpr int kParamCountOffset =
BuiltinWasmWrapperConstants::kParamCountOffset;
__ subq(rsp, Immediate(3 * kSystemPointerSize));
__ movq(MemOperand(rbp, kParamCountOffset), param_count);
__ movq(MemOperand(rbp, kInParamCountOffset), param_count);
param_count = no_reg;
// -------------------------------------------
......
......@@ -204,20 +204,18 @@ class BuiltinFrameConstants : public TypedFrameConstants {
DEFINE_TYPED_FRAME_SIZES(2);
};
// Fixed frame slots shared by the js-to-wasm wrapper, the
// ReturnPromiseOnSuspend wrapper and the WasmResume wrapper.
class BuiltinWasmWrapperConstants : public TypedFrameConstants {
public:
// This slot contains the number of slots at the top of the frame that need to
// be scanned by the GC.
static constexpr int kGCScanSlotCountOffset =
TYPED_FRAME_PUSHED_VALUE_OFFSET(0);
};
class ReturnPromiseOnSuspendFrameConstants
: public BuiltinWasmWrapperConstants {
public:
static constexpr int kParamCountOffset = TYPED_FRAME_PUSHED_VALUE_OFFSET(1);
static constexpr int kSpillAreaSize =
-(kParamCountOffset - TypedFrameConstants::kFixedFrameSizeFromFp);
// The number of parameters passed to this function.
static constexpr int kInParamCountOffset = TYPED_FRAME_PUSHED_VALUE_OFFSET(1);
// The number of parameters according to the signature.
static constexpr int kParamCountOffset = TYPED_FRAME_PUSHED_VALUE_OFFSET(2);
};
class ConstructFrameConstants : public TypedFrameConstants {
......
......@@ -87,13 +87,10 @@ load("test/mjsunit/wasm/wasm-module-builder.js");
(function TestStackSwitchSuspend() {
print(arguments.callee.name);
let builder = new WasmModuleBuilder();
builder.addGlobal(kWasmI32, true).exportAs('g');
import_index = builder.addImport('m', 'import', kSig_i_v);
builder.addFunction("test", kSig_i_v)
.addBody([
kExprCallFunction, import_index, // suspend
kExprGlobalSet, 0, // resume
kExprI32Const, 0,
]).exportFunc();
let suspender = new WebAssembly.Suspender();
function js_import() {
......@@ -107,8 +104,7 @@ load("test/mjsunit/wasm/wasm-module-builder.js");
let instance = builder.instantiate({m: {import: suspending_wasm_js_import}});
let wrapped_export = suspender.returnPromiseOnSuspend(instance.exports.test);
let combined_promise = wrapped_export();
assertEquals(0, instance.exports.g.value);
combined_promise.then(_ => assertEquals(42, instance.exports.g.value));
combined_promise.then(v => assertEquals(42, v));
})();
// Check that we can suspend back out of a resumed computation.
......@@ -193,3 +189,40 @@ load("test/mjsunit/wasm/wasm-module-builder.js");
// TODO(thibaudm): Check the result's value once this is supported.
assertEquals(42, instance.exports.g.value);
})();
(function TestStackSwitchSuspendArgs() {
print(arguments.callee.name);
function reduce(array) {
// a[0] + a[1] * 2 + a[2] * 3 + ...
return array.reduce((prev, cur, i) => prev + cur * (i + 1));
}
let builder = new WasmModuleBuilder();
// Number of param registers + 1 for both types.
let sig = makeSig([kWasmI32, kWasmI32, kWasmI32, kWasmI32, kWasmI32, kWasmI32,
kWasmF32, kWasmF32, kWasmF32, kWasmF32, kWasmF32, kWasmF32, kWasmF32], [kWasmI32]);
import_index = builder.addImport('m', 'import', sig);
builder.addFunction("test", sig)
.addBody([
kExprLocalGet, 0, kExprLocalGet, 1, kExprLocalGet, 2, kExprLocalGet, 3,
kExprLocalGet, 4, kExprLocalGet, 5, kExprLocalGet, 6, kExprLocalGet, 7,
kExprLocalGet, 8, kExprLocalGet, 9, kExprLocalGet, 10, kExprLocalGet, 11,
kExprLocalGet, 12,
kExprCallFunction, import_index, // suspend
]).exportFunc();
let suspender = new WebAssembly.Suspender();
function js_import(i1, i2, i3, i4, i5, i6, f1, f2, f3, f4, f5, f6, f7) {
return Promise.resolve(reduce(Array.from(arguments)));
};
let wasm_js_import = new WebAssembly.Function(
{parameters: ['i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'f32', 'f32',
'f32', 'f32', 'f32', 'f32', 'f32'], results: ['externref']}, js_import);
let suspending_wasm_js_import =
suspender.suspendOnReturnedPromise(wasm_js_import);
let instance = builder.instantiate({m: {import: suspending_wasm_js_import}});
let wrapped_export = suspender.returnPromiseOnSuspend(instance.exports.test);
let args = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
let combined_promise =
wrapped_export.apply(null, args);
combined_promise.then(v => assertEquals(reduce(args), v));
})();
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment