New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(rust-sdk): Context
collection
#203
Conversation
Scheme
and Context
collection
I don't think schemes are necessary, we could probably remove them in favor of the old API, while still reforming the |
Thanks! I agree that treating the Adding some way to persist a plugin like impl<'a> Plugin<'a> {
pub fn into_raw(self) -> PluginIndex {
let id = self.id;
std::mem::forget(self);
id
}
} This allows you to retain the ID of the plugin without dropping it, which means you could then opt-in to using |
Scheme
and Context
collectionContext
collection
@zshipko While that works, it feels very unnatural and unclear, not too Rust-like. Imagine you have a plugin that you want to create but not use until later: Plugin::new(...).into_raw(); Could you tell by this code that the How about we introduce a new struct named Plugin::builder()
.with_wasi(true)
.build_temp(&context) |
I want to keep the default behavior for What about instead of Inverting your |
The problem is when you retrieve a |
Ah, that's true. You could add a private field to |
You can do that but it would be odd to have a function that only works once then serves no purpose. I think the How often do people need a plugin temporarily loaded vs persisted? Is it really worth supporting an API that won't be used? In my case, I call the plugins at random times during the programs execution which is why I need it persisted. I feel like in most cases, when you need to remove a plugin, it would be coordinated at a specific time. |
The most typical use case is to create the plugin and store it somewhere to call later, then when it goes out of scope it gets destroyed just like any other Rust value. The Maybe if you shared some code from your project I could help you accomplish what you need? |
If As far as my code, it's just a system that calls all plugins sharing a protocol and aggregates the results. My code works how it is, but I don't see why I'm making a storage on top of a storage, when I can just use the inner one. Not to mention having to create my own id system to keep track of plugins. |
That sounds good to me - making I think it may require some deeper changes to the runtime but I would be curious to see what that might look like. |
Here are some ways we can return errors to the caller without a
An error would look something like this: #[repr(C)]
pub struct CreatePluginResult {
success: bool,
result: *mut Plugin,
}
impl Display for CreatePluginResult {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "Error creating Plugin")
}
} With a function to get the error string as such: pub fn error_string<T: Display>(error: T) -> *mut c_char {
todo!() // return display function
} And a macro to make defining errors easier: c_error!(
#[error("Error creating Plugin")]
CreatePluginResult: *mut Plugin,
)
An error would look something like this: #[repr(C, i32)]
pub enum Error {
CreatePluginResult = 1,
}
impl Display for CreatePluginResult {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
match *self {
Error::CreatePluginResult => write!(f, "Error creating Plugin"),
}
}
} The function to get the error string would be the same as the previous and a macro could also be made, though I don't think it'd be necessary. The only difference here that you have to remember is that the result is being passed as a pointer in the parameter. Another thing is that the library should use a crate like |
Interesting, the idea of returning You have to keep in mind that any change like this will bubble up to all the SDKs - for example, using an out parameter from Javascript is do-able, but it's tricky, Ruby has yet another way of handling it and I'm not even sure what that would look like in C# - there are reasons things are the way they are when you consider how many languages are supported. Yes the API could be nicer, but it part of the design process was making it usable from many languages. I would suggest you try to make some of the API changes you're looking for and change a few SDKs to match to get a feeling of how usable the API is from the different languages we support. |
If the language supports FFI, it surely supports out parameters, it's such a widely used C convention. Perhaps it is a little tricky to get right, but that will all be hidden from the user anyways. Also, the If anyone is familiar with FFI in any of the supported SDKs, feel free to give your thoughts. Edit: I’d also like to mention that if out parameters are really that tricky, then all the more reason to use the first approach. |
I came up with an example of what the code would look like with the new error system (1st method). macro_rules! c_error {
(
$(
$error_msg:literal: $error_arg:ty,
$error_name:ident: $result:ty,
),+
) => {
$(
#[repr(C)]
pub struct $error_name {
pub success: bool,
pub result: $result,
pub error_arg: $error_arg,
}
impl Display for $error_name {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, $error_msg, self.error_arg.unwrap())
}
}
),+
}
} Creating a new error: c_error!(
"Error creating Plugin: {:?}": Option<anyhow::Error>,
CreatePluginResult: *mut Plugin,
); What the C function would look like: #[no_mangle]
pub unsafe extern "C" fn extism_plugin_new(
wasm: *const u8,
wasm_size: Size,
with_wasi: bool,
) -> CreatePluginResult {
trace!("Call to extism_plugin_new with wasm pointer {:?}", wasm);
let data = std::slice::from_raw_parts(wasm, wasm_size as usize);
match Plugin::new(data, with_wasi) {
Ok(mut x) => {
// TODO: forget x
CreatePluginResult {
success: true,
result: &mut x as *mut _,
error_arg: None,
}},
Err(e) => {
error!("Error creating Plugin: {:?}", e);
CreatePluginResult {
success: false,
result: ptr::null_mut(),
error_arg: Some(e),
}
}
}
} I'm not a huge fan of the |
Something like this could make creating the errors easier: // success
CreatePluginResult::success(x) // convert it to pointer internally?
// error
CreatePluginResult::error(err) If you also want to save a bit of memory you can use a |
@ok-nick - I think there are is more to this PR than meets the eye here, and there's probably more context that would be helpful to know in order for us to really understand whether or not it's something we'd want to accept upstream. We created the EIP repo for these kinds of ideas, and if you would like to, I'd encourage you to flesh out these ideas a bit more in the form of a proposal. These proposals should ultimately be as thorough as EIP-005, which goes in depth about how all Extism components will be versioned, including the various components, the complexity, and the thoughtful rationale for the versioning system we've landed on. I'm going to close out this PR with preference for a proposal, and after that has been accepted, I think everyone involved here will have more clarity about the how/what/why mentioned above. Thanks! |
Example implementation of the idea proposed in #202.
The goal here is to treat
Context
as it is treated internally; a collection of plugins. This will allow users to store and call plugins whenever they want, without them being freed and without having to cache them again.A lot of the code doesn't make use of the libraries conventions, I threw it together as an example. Let me know what you think and if we should go ahead with implementing this.