Skip to content

fix: host_bindgen! returns Result instead of panicking on guest errors#1317

Open
jsturtevant wants to merge 1 commit into
mainfrom
rally/1316-host-bindgen-generated-code-panics-on-guest-error
Open

fix: host_bindgen! returns Result instead of panicking on guest errors#1317
jsturtevant wants to merge 1 commit into
mainfrom
rally/1316-host-bindgen-generated-code-panics-on-guest-error

Conversation

@jsturtevant

@jsturtevant jsturtevant commented Mar 17, 2026

Copy link
Copy Markdown
Contributor

fixes: #1316

host_bindgen! export wrappers no longer panic when Callable::call() returns Err (guest abort/trap/timeout). Host callers now receive the HyperlightError.

Breaking change

Host-side guest export traits are now fallible and use a *Exports suffix. Host functions imported by the guest keep the original WIT signatures.

Example WIT:

world test {
  import roundtrip;
  export roundtrip;
}

interface roundtrip {
  echo: func(x: string) -> string;
}

Generated/used Rust shape:

// Guest -> host import implementation: unchanged.
impl test::wit::Roundtrip for Host {
    fn echo(&mut self, x: String) -> String { x }
}

// Host -> guest export call: renamed trait and fallible result.
use test::wit::{RoundtripExports, TestExports};

let value: Result<String, HyperlightError> = sandbox.roundtrip().echo("hi".into());

Signed-off-by: James Sturtevant jsturtevant@gmail.com

@jsturtevant jsturtevant force-pushed the rally/1316-host-bindgen-generated-code-panics-on-guest-error branch from 21ca91e to eadb9f1 Compare March 17, 2026 21:19
@jsturtevant jsturtevant added the kind/bugfix For PRs that fix bugs label Mar 17, 2026

@ludfjig ludfjig left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I am fine with this, but I suspect this is intentional. I think we generally want to avoid Result types as much as possible where it makes sense, but since this matches the current guest calling convention maybe let's defer that discussion to later

@jsturtevant

Copy link
Copy Markdown
Contributor Author

I think I am fine with this, but I suspect this is intentional. I think we generally want to avoid Result types as much as possible where it makes sense, but since this matches the current guest calling convention maybe let's defer that discussion to later

Open to other ways of addressing this I guess but a panic in the guest shouldn't panic this host. It seemed practical to match existing behavoir

@syntactically

Copy link
Copy Markdown
Member

I agree with @ludfjig that, generally speaking, the semantic gap of lifting everything from the interface into Option isn't particularly attractive. However, I do see the need to deal with errors that fundamentally can happen during execution on the host side.

Is there a reason that this is also applied to the host functions imported by the guest? I think that generally speaking it is the ability to export a function implementing an interface, but actually implement a different interface (i.e. one with more optionality) that is the most semantically damaging---so the implicit Results are particularly unattractive on guest->host function calls (and to some extent in the implementation of guest functions, although the ability to trap/panic/etc is perhaps equivalent). It seems like splitting this (so that host->guest function calls can return errors, but host functions can't be implemented by returning errors) is what is common in the prior art e.g. the wasmtime host bindgen. Is it just about continuing to be able to use the same trait for the import and export versions of an interface? Maybe a type parameter to track whether this is an imported or exported usage would make sense?

Let me know what you think about the usability of that. As I think about this more, from the wasm perspective especially, I do think that having the Results on host function calls is slightly unattractive, but it's not quite as bad as I would have guessed, since from wasm's perspective, a (wasm) import that's called always does in fact have the option of causing the entire world to be torn town via purposefully causing a trap, and this is perhaps in some ways similar behaviour.

@jsturtevant

Copy link
Copy Markdown
Contributor Author

Is there a reason that this is also applied to the host functions imported by the guest?

This was to match the current non-wit implementation. We had improved the situation with errors in #868.

I think that generally speaking it is the ability to export a function implementing an interface, but actually implement a different interface (i.e. one with more optionality) that is the most semantically damaging---so the implicit Results are particularly unattractive on guest->host function calls (and to some extent in the implementation of guest functions, although the ability to trap/panic/etc is perhaps equivalent). It seems like splitting this (so that host->guest function calls can return errors, but host functions can't be implemented by returning errors) is what is common in the prior art e.g. the wasmtime host bindgen.

Agree with the general sentiment that's its not ideal. I am not so sure we don't want host function to return errors, it seems useful to me to be able to say something went wrong across the boundry. Be interesting in what others think

@jsturtevant

Copy link
Copy Markdown
Contributor Author

Agree with the general sentiment that's its not ideal. I am not so sure we don't want host function to return errors, it seems useful to me to be able to say something went wrong across the boundry. Be interesting in what others think

actually, was thinking on this some more and WIT does have the ability capture "result" types so we probalby don't need this on the guest side (I don't see the equivalent ::std::result::Result::Ok(#ret) = #ret else { panic!("bad return from guest {:?}", #ret) }; on the Guest->Host calls. Maybe there is another way to handle that too... I am open to ideas just really think we shouldn't panic if guest traps.

@syntactically

Copy link
Copy Markdown
Member

just really think we shouldn't panic if guest traps

I'm with you on that one :)

This was to match the current non-wit implementation. We had improved the situation with errors in #868.

I am not so sure we don't want host function to return errors, it seems useful to me to be able to say something went wrong across the boundry.

I'm still not totally convinced on this, although I can see the arguments. Can you elaborate a little more on the use cases you see? If we did allow this, I think it's important that it not be semantically visible in the guest, which shouldn't know whether its imported component is implemented across a hyperlight-host boundary or by any other component, so we would need to propagate the error return up, and I would think that it would probably make sense to poison the sandbox (probably by panicking the guest?). If there's not a super strong use case, having the optionality just on the host->guest calls and not the hostcall returns seems like it is a starting point that gets rid of the main issue and is minimally invasive?

actually, was thinking on this some more and WIT does have the ability capture "result" types so we probalby don't need this on the guest side (I don't see the equivalent ::std::result::Result::Ok(#ret) = #ret else { panic!("bad return from guest {:?}", #ret) }; on the Guest->Host calls. Maybe there is another way to handle that too...

There is an unwrap on the host call result here.

Precisely the thing that I want to be careful about here is conflating WIT-level semantically-there-in-the-API optionality with this unconditionally-added-everywhere Hyperlight-level optionality. As I said in the paragraph above, even if we did add this for host function return types, I would not want to see it meaningfully exposed to the guest code.

@jsturtevant

Copy link
Copy Markdown
Contributor Author

I am not so sure we don't want host function to return errors, it seems useful to me to be able to say something went wrong across the boundry.

I'm still not totally convinced on this, although I can see the arguments. Can you elaborate a little more on the use cases you see? If we did allow this, I think it's important that it not be semantically visible in the guest, which shouldn't know whether its imported component is implemented across a hyperlight-host boundary or by any other component, so we would need to propagate the error return up, and I would think that it would probably make sense to poison the sandbox (probably by panicking the guest?). If there's not a super strong use case, having the optionality just on the host->guest calls and not the hostcall returns seems like it is a starting point that gets rid of the main issue and is minimally invasive?

Yes I am in agree that we shouldn't do this now. I'm going to revisit this and see if I can come up with something different for consideration.

@ludfjig

ludfjig commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

@jsturtevant should we close this one?

@jsturtevant

Copy link
Copy Markdown
Contributor Author

We need a version of this, let me see if I can fix it up this week

Copilot AI review requested due to automatic review settings June 19, 2026 23:01
@jsturtevant jsturtevant force-pushed the rally/1316-host-bindgen-generated-code-panics-on-guest-error branch from eadb9f1 to 498499a Compare June 19, 2026 23:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the host_bindgen!-generated host-side export wrappers so that guest call failures (e.g., abort/trap/timeout) are surfaced to callers as Result<_, HyperlightError> instead of crashing the host via panic!. This aligns exported guest calls with other Hyperlight call paths that already return errors.

Changes:

  • Change host export wrapper generation to propagate Callable::call() failures via ? and return Ok(...) on success.
  • Update generated host-side export trait method signatures to return Result<T, HyperlightError> (while keeping constructors unaffected).
  • Update tests to unwrap successful guest calls and add a synthetic error-propagation test; also add some codegen deduplication state to avoid duplicate emitted types.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/hyperlight_host/tests/wit_test.rs Updates callers for new Result-returning export methods and adds a test ensuring errors are returned instead of panicking.
src/hyperlight_component_util/src/rtypes.rs Changes generated host-side export trait method return types to Result<_, HyperlightError> and adjusts export-instance trait naming.
src/hyperlight_component_util/src/resource.rs Makes generated resource-table new() constructor pub(crate) (needed by new/updated usage patterns).
src/hyperlight_component_util/src/host.rs Removes panic! on guest call errors in generated wrappers; returns Result and propagates failures with ?.
src/hyperlight_component_util/src/emit.rs Adds module-level tracking (emitted_type_names) to prevent duplicate type emissions during codegen.

Comment thread src/hyperlight_component_util/src/rtypes.rs
Comment thread src/hyperlight_host/tests/wit_test.rs
Make generated host-side WIT export calls return HyperlightError instead of panicking when guest execution fails, while keeping guest-imported host function traits non-fallible. Add a real WIT guest panic export to cover the host error path.

Signed-off-by: James Sturtevant <jsturtevant@gmail.com>
@jsturtevant jsturtevant force-pushed the rally/1316-host-bindgen-generated-code-panics-on-guest-error branch from cefb1ac to cae31c2 Compare June 19, 2026 23:53
@jsturtevant

Copy link
Copy Markdown
Contributor Author

I refreshed this. I wasn't able to find a way to not do this on host side but it is minimally invasive I believe. On the guest side we can use wit interface as discussed above.

@jsturtevant jsturtevant added the ready-for-review PR is ready for (re-)review label Jun 23, 2026

@danbugs danbugs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly LGTM. Just minor nits.

Comment on lines +644 to +648
let result = if !s.is_guest && s.is_export {
quote! { ::std::result::Result<#result, ::hyperlight_host::error::HyperlightError> }
} else {
result
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want a helper for this? It seems to repeat enough times.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be pushed into emit_func_result, whose semantic job is to centralize the logic for converting a C-M result type into a Rust type for bindings generation.

Are there any times where we use that where we don't want it?

let ::std::result::Result::Ok(#ret) = #ret else { panic!("bad return from guest {:?}", #ret) };
let #ret = #ret?;
#[allow(clippy::unused_unit)]
let mut rts = self.rt.lock().unwrap();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will still panic--fine?

syntactically
syntactically previously approved these changes Jun 25, 2026

@syntactically syntactically left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to make sure I understand, is the point of adding the Exports onto the end of the trait name on the host side just to make sure that if the same interface is used as an import and an export, the import and export don't collide?

Is there any barrier to centralizing the logic about the function result types in emit_func_result, whose job it usually is to decide the function result types?

pub items: TokenStream,
pub traits: BTreeMap<Ident, Trait>,
pub impls: BTreeMap<(Vec<Ident>, Ident), TokenStream>,
pub emitted_type_names: BTreeSet<Ident>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow what this is being used for?

Comment on lines +644 to +648
let result = if !s.is_guest && s.is_export {
quote! { ::std::result::Result<#result, ::hyperlight_host::error::HyperlightError> }
} else {
result
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be pushed into emit_func_result, whose semantic job is to centralize the logic for converting a C-M result type into a Rust type for bindings generation.

Are there any times where we use that where we don't want it?

) -> TokenStream {
let id = kebab_to_type(ed.kebab_name);
let mut s = s.helper();
if !s.cur_mod().emitted_type_names.insert(id.clone()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this being used for? It seems like it will simply drop some definitions, which doesn't seem ideal?

@syntactically syntactically dismissed their stale review June 25, 2026 22:25

Intended to hit "comment" rather than "approve"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/bugfix For PRs that fix bugs ready-for-review PR is ready for (re-)review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

host_bindgen! generated code panics on guest errors instead of returning Result

5 participants