feat(plugin): Add PluginError (#3300)

This commit is contained in:
OJ Kwon 2022-01-17 19:26:04 -08:00 committed by GitHub
parent 5a08327784
commit c6ffdc8717
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 165 additions and 51 deletions

View File

@ -8,6 +8,34 @@ use crate::{syntax_pos::Mark, SyntaxContext};
use anyhow::Error;
use std::any::type_name;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
#[cfg_attr(
feature = "plugin-base",
derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
)]
/// Enum for possible errors while running transform via plugin.
/// Mostly this enum is being used to bubble up error occurred inside of plugin
/// runtime which we can't transparently pass forward Error itself. Host (SWC)
/// will try to attach context if possible then raise it as Error.
pub enum PluginError {
/// Occurs when failed to convert size passed from host / guest into usize
/// or similar for the conversion. This is an internal error rasied via
/// plugin_macro, noramlly plugin author should not raise this manually.
SizeInteropFailure(String),
/// Occurs when failed to reconstruct a struct from `Serialized`.
Deserialize((String, Vec<u8>)),
/// Occurs when failed to serialize a struct into `Serialized`.
/// Unlike deserialize error, this error cannot forward any context for the
/// raw bytes: when serialze failed, there's nothing we can pass between
/// runtime.
Serialize(String),
/// When plugin fails to transform for some reason.
/// This is incomplete yet as it is unclear what kind of data plugin want to
/// forward into host.
Transform,
}
/// Wraps internal representation of serialized data. Consumers should not
/// rely on specific details of byte format struct contains: it is
/// strictly implementation detail which can change anytime.

View File

@ -1,5 +1,9 @@
// Reexports
pub use swc_common::{chain, plugin::Serialized, DUMMY_SP};
pub use swc_common::{
chain,
plugin::{PluginError, Serialized},
DUMMY_SP,
};
pub mod ast {
pub use swc_atoms::*;
pub use swc_ecma_ast::*;

View File

@ -1,14 +0,0 @@
[package]
authors = ["강동윤 <kdy1997.dev@gmail.com>"]
description = "Base for swc plugins"
documentation = "https://rustdoc.swc.rs/swc_plugin_build/"
edition = "2021"
license = "Apache-2.0"
name = "swc_plugin_build"
repository = "https://github.com/swc-project/swc.git"
version = "0.1.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.41"

View File

@ -1 +0,0 @@
pub fn setup() {}

View File

@ -25,41 +25,101 @@ fn handle_func(func: ItemFn) -> TokenStream {
let ret = quote! {
#func
use swc_plugin::Serialized;
/// Internal function plugin_macro uses to create ptr to PluginError.
fn construct_error_ptr(plugin_error: PluginError) -> (i32, i32, i32) {
let ret = swc_plugin::Serialized::serialize(&plugin_error).expect("Should able to serialize PluginError");
let ret_ref = ret.as_ref();
// TODO:
// error handling & bubbling
(
1,
ret_ref.as_ptr() as _,
ret_ref.len().try_into().expect("Should able to convert size of PluginError")
)
}
// Macro to allow compose plugin's transform function without manual pointer operation.
// Internally it wraps pointer operation also bubbles up error in forms of PluginError.
// There are some cases error won't be wrapped up however - for example, we expect
// serialization of PluginError itself should succeed.
#[no_mangle]
pub fn #process_impl_ident(ast_ptr: *const u8, ast_ptr_len: i32, config_str_ptr: *const u8, config_str_ptr_len: i32) -> (i32, i32) {
pub fn #process_impl_ident(ast_ptr: *const u8, ast_ptr_len: i32, config_str_ptr: *const u8, config_str_ptr_len: i32) -> (i32, i32, i32) {
let ast_ptr_len_usize: Result<usize, std::num::TryFromIntError> = ast_ptr_len.try_into();
let config_str_ptr_len_usize: Result<usize, std::num::TryFromIntError> = config_str_ptr_len.try_into();
if ast_ptr_len_usize.is_err() {
let err = swc_plugin::PluginError::SizeInteropFailure("Failed to convert size of AST pointer".to_string());
return construct_error_ptr(err);
}
if config_str_ptr_len_usize.is_err() {
let err = swc_plugin::PluginError::SizeInteropFailure("Failed to convert size of plugin config string pointer".to_string());
return construct_error_ptr(err);
}
// Read raw serialized bytes from wasm's memory space. Host (SWC) should
// allocate memory, copy bytes and pass ptr to plugin.
let raw_ast_serialized_bytes =
unsafe { std::slice::from_raw_parts(ast_ptr, ast_ptr_len.try_into().unwrap()) };
unsafe { std::slice::from_raw_parts(ast_ptr, ast_ptr_len_usize.unwrap()) };
let raw_config_serialized_bytes =
unsafe { std::slice::from_raw_parts(config_str_ptr, config_str_ptr_len.try_into().unwrap()) };
unsafe { std::slice::from_raw_parts(config_str_ptr, config_str_ptr_len_usize.unwrap()) };
// Reconstruct SerializedProgram from raw bytes
let serialized_program = Serialized::new_for_plugin(raw_ast_serialized_bytes, ast_ptr_len);
let serialized_config = Serialized::new_for_plugin(raw_config_serialized_bytes, config_str_ptr_len);
let serialized_program = swc_plugin::Serialized::new_for_plugin(raw_ast_serialized_bytes, ast_ptr_len);
let serialized_config = swc_plugin::Serialized::new_for_plugin(raw_config_serialized_bytes, config_str_ptr_len);
// TODO: Returning error pointer instead of unwrap
let program: Program =
Serialized::deserialize(&serialized_program).expect("Should able to deserialize");
let config: String = Serialized::deserialize(&serialized_config).expect("Should able to deserialize");
// Reconstruct `Program` & config string from serialized program
let program = swc_plugin::Serialized::deserialize(&serialized_program);
let config = swc_plugin::Serialized::deserialize(&serialized_config);
// Take original plugin fn ident, then call it with interop-ed args
let transformed_program = #ident(program, config).expect("Should return program for now");
if program.is_err() {
let err = swc_plugin::PluginError::Deserialize(
("Failed to deserialize program received from host".to_string(),
raw_ast_serialized_bytes.to_vec())
);
return construct_error_ptr(err);
}
let program = program.expect("Should be a program");
if config.is_err() {
let err = swc_plugin::PluginError::Deserialize(
("Failed to deserialize config string received from host".to_string(),
raw_config_serialized_bytes.to_vec())
);
return construct_error_ptr(err);
}
let config = config.expect("Should be a string");
// Take original plugin fn ident, then call it with interop'ed args
let transformed_program = #ident(program, config);
if transformed_program.is_err() {
let err = transformed_program.expect_err("Should be an error");
return construct_error_ptr(err);
}
let transformed_program = transformed_program.expect("Should be a transformed program");
// Serialize transformed result, return back to the host.
let serialized_result =
Serialized::serialize(&transformed_program).expect("Should serializable");
let serialized_result = swc_plugin::Serialized::serialize(&transformed_program);
if serialized_result.is_err() {
let err = swc_plugin::PluginError::Serialize("Failed to serialize transformed program".to_string());
return construct_error_ptr(err);
}
let serialized_result = serialized_result.expect("Should be a realized transformed program");
let serialized_result = serialized_result.as_ref();
let serialized_result_len: Result<i32, std::num::TryFromIntError> = serialized_result.len().try_into();
if serialized_result_len.is_err() {
let err = swc_plugin::PluginError::SizeInteropFailure("Failed to convert size of transformed AST pointer".to_string());
return construct_error_ptr(err);
}
(
0,
serialized_result.as_ptr() as _,
serialized_result.len().try_into().unwrap(),
serialized_result_len.expect("Should be an i32"),
)
}
};

View File

@ -7,7 +7,10 @@ use anyhow::{anyhow, Context, Error};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use resolve::PluginCache;
use swc_common::{collections::AHashMap, plugin::Serialized};
use swc_common::{
collections::AHashMap,
plugin::{PluginError, Serialized},
};
use wasmer::{imports, Array, Exports, Instance, Memory, MemoryType, Module, Store, WasmPtr};
use wasmer_cache::{Cache, Hash};
use wasmer_wasi::{get_wasi_version, WasiState};
@ -140,7 +143,7 @@ fn load_plugin(plugin_path: &Path, cache: &mut Option<PluginCache>) -> Result<In
/// teardown
struct PluginTransformTracker {
// Main transform interface plugin exports
exported_plugin_transform: wasmer::NativeFunc<(i32, i32, i32, i32), (i32, i32)>,
exported_plugin_transform: wasmer::NativeFunc<(i32, i32, i32, i32), (i32, i32, i32)>,
// `__free` function automatically exported via swc_plugin sdk to allow deallocation in guest
// memory space
exported_plugin_free: wasmer::NativeFunc<(i32, i32), i32>,
@ -159,7 +162,9 @@ impl PluginTransformTracker {
let tracker = PluginTransformTracker {
exported_plugin_transform: instance
.exports
.get_native_function::<(i32, i32, i32, i32), (i32, i32)>("__plugin_process_impl")?,
.get_native_function::<(i32, i32, i32, i32), (i32, i32, i32)>(
"__plugin_process_impl",
)?,
exported_plugin_free: instance
.exports
.get_native_function::<(i32, i32), i32>("__free")?,
@ -209,7 +214,12 @@ impl PluginTransformTracker {
/// Copy guest's memory into host, construct serialized struct from raw
/// bytes.
fn read_bytes_from_guest(&mut self, returned_ptr: i32, len: i32) -> Result<Serialized, Error> {
fn read_bytes_from_guest(
&mut self,
returned_ptr_result: i32,
returned_ptr: i32,
len: i32,
) -> Result<Serialized, Error> {
let memory = self.instance.exports.get_memory("memory")?;
let ptr: WasmPtr<u8, Array> = WasmPtr::new(returned_ptr as _);
@ -225,7 +235,26 @@ impl PluginTransformTracker {
.map(|(_size, cell)| cell.get())
.collect::<Vec<u8>>();
Ok(Serialized::new_for_plugin(&transformed_raw_bytes[..], len))
let ret = Serialized::new_for_plugin(&transformed_raw_bytes[..], len);
if returned_ptr_result == 0 {
Ok(ret)
} else {
let err: PluginError = Serialized::deserialize(&ret)?;
match err {
PluginError::SizeInteropFailure(msg) => Err(anyhow!(
"Failed to convert pointer size to calculate: {}",
msg
)),
PluginError::Deserialize((msg, ..)) | PluginError::Serialize(msg) => {
Err(anyhow!("{}", msg))
}
PluginError::Transform => Err(anyhow!("Failed to apply transform via plugin")),
_ => Err(anyhow!(
"Unexpected error occurred while running plugin transform"
)),
}
}
}
fn transform(
@ -243,8 +272,10 @@ impl PluginTransformTracker {
config_str_ptr.1,
)?;
self.allocated_ptr_vec.push(returned_ptr);
self.read_bytes_from_guest(returned_ptr.0, returned_ptr.1)
self.allocated_ptr_vec
.push((returned_ptr.1, returned_ptr.2));
self.read_bytes_from_guest(returned_ptr.0, returned_ptr.1, returned_ptr.2)
}
}
@ -258,8 +289,6 @@ impl Drop for PluginTransformTracker {
}
}
// TODO
// - error propagation from plugin
pub fn apply_js_plugin(
plugin_name: &str,
path: &Path,

View File

@ -20,8 +20,11 @@ fn get_ids_by_name(module: &Module, function_name: &str) -> (ExportId, FunctionI
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// We'll polyfill return type of plugin's `process` interface to (i32, i32)
let transformations = vec![("__plugin_process_impl", vec![ValType::I32, ValType::I32])];
// We'll polyfill return type of plugin's `process` interface to (i32, i32, i32)
let transformations = vec![(
"__plugin_process_impl",
vec![ValType::I32, ValType::I32, ValType::I32],
)];
let target: Vec<String> = env::args().collect();

View File

@ -1,4 +1,4 @@
use swc_plugin::{ast::*, plugin_module, DUMMY_SP};
use swc_plugin::{ast::*, plugin_module, PluginError, DUMMY_SP};
struct ConsoleOutputReplacer;
@ -28,13 +28,18 @@ impl VisitMut for ConsoleOutputReplacer {
/// returning ptr back to host.
///
/// It is possible to opt out from macro by writing transform fn manually via
/// `__plugin_process_impl(*const u8, i32, *const u8, i32) -> (i32,i32)`
/// if plugin need to handle low-level ptr directly.
/// `__plugin_process_impl(
/// ast_ptr: *const u8,
/// ast_ptr_len: i32,
/// config_str_ptr: *const u8,
/// config_str_ptr_len: i32) ->
/// (is_ok: i32 /* 0 means success */,
/// result_ptr: i32,
/// result_ptr_length: i32)`
///
/// TODO:
/// - better developer experience: println / dbg!() doesn't emit anything
/// if plugin need to handle low-level ptr directly.
#[plugin_module]
pub fn process(program: Program, _plugin_config: String) -> Result<Program, std::fmt::Error> {
pub fn process(program: Program, _plugin_config: String) -> Result<Program, PluginError> {
let transformed_program = program.fold_with(&mut as_folder(ConsoleOutputReplacer));
Ok(transformed_program)
}