Skip to content

Commit a79694d

Browse files
committed
feat(virtq): add with_hint function call API
Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com>
1 parent 6a85ad1 commit a79694d

5 files changed

Lines changed: 192 additions & 20 deletions

File tree

src/hyperlight_guest/src/virtq/context.rs

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,35 @@ impl GuestContext {
118118

119119
/// Call a host function via the G2H virtqueue.
120120
///
121-
/// The reply guard is checked before submitting the readwrite chain
122-
/// to ensure G2H capacity is reserved for pending responses.
121+
/// Uses the default completion capacity (4096 bytes) for the response
122+
/// buffer. For host functions known to return large payloads, use
123+
/// [`call_host_function_with_hint`](Self::call_host_function_with_hint).
123124
pub fn call_host_function<T: TryFrom<ReturnValue>>(
124125
&mut self,
125126
function_name: &str,
126127
parameters: Option<Vec<ParameterValue>>,
127128
return_type: ReturnType,
129+
) -> Result<T> {
130+
self.call_host_function_with_hint(
131+
function_name,
132+
parameters,
133+
return_type,
134+
self.g2h_response_cap,
135+
)
136+
}
137+
138+
/// Call a host function with an explicit response capacity hint.
139+
///
140+
/// `resp_hint` is the total completion buffer size in bytes
141+
/// (including wire overhead: VirtqMsgHeader + FlatBuffer framing).
142+
/// The BufferPool allocates multiple adjacent slots when the hint
143+
/// exceeds a single slot size, so this is zero-copy for the host.
144+
pub fn call_host_function_with_hint<T: TryFrom<ReturnValue>>(
145+
&mut self,
146+
function_name: &str,
147+
parameters: Option<Vec<ParameterValue>>,
148+
return_type: ReturnType,
149+
resp_hint: usize,
128150
) -> Result<T> {
129151
let params = parameters.as_deref().unwrap_or_default();
130152
let estimated_capacity = estimate_flatbuffer_capacity(function_name, params);
@@ -140,15 +162,15 @@ impl GuestContext {
140162
let payload = fc.encode(&mut builder);
141163

142164
let reqid = REQUEST_ID.fetch_add(1, Relaxed);
143-
let hdr = VirtqMsgHeader::new(MsgKind::Request, reqid, payload.len() as u32);
144-
let hdr_bytes = bytemuck::bytes_of(&hdr);
165+
let msg = VirtqMsgHeader::new(MsgKind::Request, reqid, payload.len() as u32);
166+
let hdr = bytemuck::bytes_of(&msg);
145167

146168
let entry_len = VirtqMsgHeader::SIZE + payload.len();
147169

148170
// Reply guard: readwrite chains use 2 descriptors, leave room for pending replies.
149171
self.ensure_reply_capacity(2)?;
150172

151-
let token = match self.try_send_readwrite(hdr_bytes, payload, entry_len) {
173+
let token = match self.try_send_readwrite(hdr, payload, entry_len, resp_hint) {
152174
Ok(tok) => tok,
153175
Err(e) if e.is_transient() => {
154176
self.g2h_producer.notify_backpressure();
@@ -157,7 +179,7 @@ impl GuestContext {
157179
bail!("G2H reclaim: {err}");
158180
}
159181

160-
let Ok(tok) = self.try_send_readwrite(hdr_bytes, payload, entry_len) else {
182+
let Ok(tok) = self.try_send_readwrite(hdr, payload, entry_len, resp_hint) else {
161183
bail!("G2H call retry");
162184
};
163185

@@ -436,12 +458,13 @@ impl GuestContext {
436458
header: &[u8],
437459
payload: &[u8],
438460
entry_len: usize,
461+
completion_cap: usize,
439462
) -> result::Result<Token, virtq::VirtqError> {
440463
let mut entry = self
441464
.g2h_producer
442465
.chain()
443466
.entry(entry_len)
444-
.completion(self.g2h_response_cap)
467+
.completion(completion_cap)
445468
.build()?;
446469

447470
entry.write_all(header)?;

src/hyperlight_guest_bin/src/host_comm.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,24 @@ where
4747
virtq::with_context(|ctx| ctx.call_host_function(function_name, parameters, return_type))
4848
}
4949

50+
/// Call a host function with an explicit response capacity hint.
51+
///
52+
/// `response_hint` is the total completion buffer size in bytes including wire overhead.
53+
/// Use this when you know the host function returns a large payload (e.g., >4096 bytes).
54+
pub fn call_host_function_with_hint<T>(
55+
function_name: &str,
56+
parameters: Option<Vec<ParameterValue>>,
57+
return_type: ReturnType,
58+
response_hint: usize,
59+
) -> Result<T>
60+
where
61+
T: TryFrom<ReturnValue>,
62+
{
63+
virtq::with_context(|ctx| {
64+
ctx.call_host_function_with_hint(function_name, parameters, return_type, response_hint)
65+
})
66+
}
67+
5068
pub fn call_host<T>(function_name: impl AsRef<str>, args: impl ParameterTuple) -> Result<T>
5169
where
5270
T: SupportedReturnType + TryFrom<ReturnValue>,

src/hyperlight_host/src/sandbox/outb.rs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -273,29 +273,31 @@ fn outb_virtq_call(
273273
.try_lock()
274274
.map_err(|e| HandleOutbError::LockFailed(file!(), line!(), e.to_string()))?;
275275

276-
let mut res = registry
276+
let res = registry
277277
.call_host_function(&name, args)
278278
.map_err(|e| GuestError::new(ErrorCode::HostFunctionError, e.to_string()));
279279

280-
// Truncate oversized error messages so the serialized response
281-
// fits in the completion buffer the guest pre-allocated.
282-
if let Err(err) = &mut res
283-
&& err.message.len() > wc.capacity()
284-
{
285-
err.message.truncate(wc.capacity());
286-
}
287-
288-
// Serialize response: VirtqMsgHeader + FunctionCallResult
289280
let func_result = FunctionCallResult::new(res);
290281
let mut builder = flatbuffers::FlatBufferBuilder::new();
291-
let result_payload = func_result.encode(&mut builder);
282+
let mut result_payload = func_result.encode(&mut builder).to_vec();
283+
284+
let total = VirtqMsgHeader::SIZE + result_payload.len();
285+
if total > wc.capacity() {
286+
let too_large = GuestError::new(
287+
ErrorCode::HostFunctionError,
288+
"response too large for completion buffer".into(),
289+
);
290+
let fallback = FunctionCallResult::new(Err(too_large));
291+
let mut fb = flatbuffers::FlatBufferBuilder::new();
292+
result_payload = fallback.encode(&mut fb).to_vec();
293+
}
292294

293295
let resp_header = VirtqMsgHeader::new(MsgKind::Response, 0, result_payload.len() as u32);
294296
let resp_header_bytes = bytemuck::bytes_of(&resp_header);
295297

296298
wc.write_all(resp_header_bytes)
297299
.map_err(|e| HandleOutbError::WriteHostFunctionResponse(format!("{e}")))?;
298-
wc.write_all(result_payload)
300+
wc.write_all(&result_payload)
299301
.map_err(|e| HandleOutbError::WriteHostFunctionResponse(format!("{e}")))?;
300302
consumer
301303
.complete(wc.into())

src/hyperlight_host/tests/sandbox_host_tests.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,3 +564,88 @@ fn virtq_multi_descriptor_h2g_repeated_calls() {
564564
}
565565
});
566566
}
567+
568+
/// Helper to create a sandbox with a "GetLargeResponse" host function
569+
/// that returns `size` bytes filled with 0xAB.
570+
fn sandbox_with_large_response(cfg: SandboxConfiguration) -> MultiUseSandbox {
571+
let mut sandbox = UninitializedSandbox::new(
572+
GuestBinary::FilePath(simple_guest_as_string().unwrap()),
573+
Some(cfg),
574+
)
575+
.unwrap();
576+
sandbox
577+
.register("GetLargeResponse", |size: i32| -> Result<Vec<u8>> {
578+
Ok(vec![0xABu8; size as usize])
579+
})
580+
.unwrap();
581+
sandbox.evolve().unwrap()
582+
}
583+
584+
#[test]
585+
fn virtq_large_g2h_response_with_hint() {
586+
// Host function returns >4096 bytes. Guest uses a sized completion
587+
// hint so the pool allocates multiple adjacent slots.
588+
let mut cfg = SandboxConfiguration::default();
589+
cfg.set_g2h_pool_pages(16);
590+
let mut sandbox = sandbox_with_large_response(cfg);
591+
592+
// 8000 bytes of response payload. With FlatBuffer + header overhead,
593+
// the wire size is ~8100 bytes. A hint of 3*4096 = 12288 is enough.
594+
let hint = 3 * 4096i32;
595+
let res: Vec<u8> = sandbox
596+
.call("CallGetLargeResponseWithHint", (8000i32, hint))
597+
.unwrap();
598+
assert_eq!(res.len(), 8000);
599+
assert!(res.iter().all(|&b| b == 0xAB));
600+
}
601+
602+
#[test]
603+
fn virtq_large_g2h_response_too_large_without_hint() {
604+
// Without a hint, the default 4096-byte completion buffer is used.
605+
// A response >4096 bytes should trigger the host's "response too
606+
// large" fallback error instead of a transport crash.
607+
let mut cfg = SandboxConfiguration::default();
608+
cfg.set_g2h_pool_pages(16);
609+
let mut sandbox = sandbox_with_large_response(cfg);
610+
611+
let res = sandbox.call::<Vec<u8>>("CallGetLargeResponseDefault", 8000i32);
612+
assert!(
613+
res.is_err(),
614+
"expected error for oversized response without hint"
615+
);
616+
}
617+
618+
#[test]
619+
fn virtq_large_g2h_response_boundary() {
620+
// Response that fits exactly in one page (with overhead) should work
621+
// without needing a hint.
622+
let mut cfg = SandboxConfiguration::default();
623+
cfg.set_g2h_pool_pages(16);
624+
let mut sandbox = sandbox_with_large_response(cfg);
625+
626+
// Small response that fits in default 4096 buffer
627+
let res: Vec<u8> = sandbox
628+
.call("CallGetLargeResponseDefault", 1000i32)
629+
.unwrap();
630+
assert_eq!(res.len(), 1000);
631+
assert!(res.iter().all(|&b| b == 0xAB));
632+
}
633+
634+
#[test]
635+
fn virtq_large_g2h_response_after_log_backpressure() {
636+
// Logs fill the G2H pool, then a large host response (with hint)
637+
// needs multi-slot allocation. The backpressure path must drain
638+
// completed log entries to free pool slots for the large completion.
639+
let mut cfg = SandboxConfiguration::default();
640+
cfg.set_g2h_pool_pages(16);
641+
let mut sandbox = sandbox_with_large_response(cfg);
642+
643+
// Emit 20 log entries to consume pool slots, then request 8KB
644+
// response with a 12KB hint (3 upper-slab slots).
645+
let hint = 3 * 4096i32;
646+
let res: Vec<u8> = sandbox
647+
.call("LogThenLargeResponse", (20i32, 8000i32, hint))
648+
.unwrap();
649+
assert_eq!(res.len(), 8000);
650+
assert!(res.iter().all(|&b| b == 0xAB));
651+
}

src/tests/rust_guests/simpleguest/src/main.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ use hyperlight_guest_bin::exception::arch::{Context, ExceptionInfo};
4949
use hyperlight_guest_bin::guest_function::definition::{GuestFunc, GuestFunctionDefinition};
5050
use hyperlight_guest_bin::guest_function::register::register_function;
5151
use hyperlight_guest_bin::host_comm::{
52-
call_host_function, print_output_with_host_print, read_n_bytes_from_user_memory,
52+
call_host_function, call_host_function_with_hint, print_output_with_host_print,
53+
read_n_bytes_from_user_memory,
5354
};
5455
use hyperlight_guest_bin::memory::malloc;
5556
use hyperlight_guest_bin::{guest_function, guest_logger, host_function};
@@ -379,6 +380,49 @@ fn echo(value: String) -> String {
379380
value
380381
}
381382

383+
/// Calls a host function "GetLargeResponse" with an explicit response
384+
/// capacity hint. The `hint` parameter is the total completion buffer
385+
/// size in bytes.
386+
#[guest_function("CallGetLargeResponseWithHint")]
387+
fn call_get_large_response_with_hint(size: i32, hint: i32) -> Vec<u8> {
388+
call_host_function_with_hint::<Vec<u8>>(
389+
"GetLargeResponse",
390+
Some(vec![ParameterValue::Int(size)]),
391+
ReturnType::VecBytes,
392+
hint as usize,
393+
)
394+
.expect("GetLargeResponse call failed")
395+
}
396+
397+
/// Calls a host function "GetLargeResponse" WITHOUT a hint, using the
398+
/// default 4096-byte completion buffer.
399+
#[guest_function("CallGetLargeResponseDefault")]
400+
fn call_get_large_response_default(size: i32) -> Vec<u8> {
401+
call_host_function::<Vec<u8>>(
402+
"GetLargeResponse",
403+
Some(vec![ParameterValue::Int(size)]),
404+
ReturnType::VecBytes,
405+
)
406+
.expect("GetLargeResponse call failed")
407+
}
408+
409+
/// Emits `log_count` log entries to fill the G2H queue, then calls
410+
/// "GetLargeResponse" with a sized hint. Tests that backpressure
411+
/// draining of logs frees pool slots for the large completion.
412+
#[guest_function("LogThenLargeResponse")]
413+
fn log_then_large_response(log_count: i32, size: i32, hint: i32) -> Vec<u8> {
414+
for i in 0..log_count {
415+
log::info!("backpressure log {}", i);
416+
}
417+
call_host_function_with_hint::<Vec<u8>>(
418+
"GetLargeResponse",
419+
Some(vec![ParameterValue::Int(size)]),
420+
ReturnType::VecBytes,
421+
hint as usize,
422+
)
423+
.expect("GetLargeResponse after logs failed")
424+
}
425+
382426
#[guest_function("GetSizePrefixedBuffer")]
383427
fn get_size_prefixed_buffer(data: Vec<u8>) -> Vec<u8> {
384428
data

0 commit comments

Comments
 (0)