-
-
Notifications
You must be signed in to change notification settings - Fork 892
avm2: Add #[native]
macro to allow defining methods with native types
#7895
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
Open
Aaron1011
wants to merge
1
commit into
ruffle-rs:master
Choose a base branch
from
Aaron1011:min-native-macro
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
use proc_macro::TokenStream; | ||
use proc_macro2::{Ident, Span}; | ||
use quote::{quote, quote_spanned, ToTokens}; | ||
use syn::spanned::Spanned; | ||
use syn::{parse_macro_input, FnArg, ItemFn, Pat}; | ||
|
||
// A `#[native]` method is of the form `fn(&mut Activation, ReceiverType, Arg0Type, Arg1Type, ...) | ||
// When looking for arguments, we skip over the first 2 parameters. | ||
const NUM_NON_ARG_PARAMS: usize = 2; | ||
|
||
pub fn native_impl(_args: TokenStream, item: TokenStream) -> TokenStream { | ||
let input = parse_macro_input!(item as ItemFn); | ||
let vis = &input.vis; | ||
let method_name = &input.sig.ident; | ||
|
||
// Extracts `(name, Type)` from a parameter definition of the form `name: Type` | ||
let get_param_ident_ty = |arg| { | ||
let arg: &FnArg = arg; | ||
match arg { | ||
FnArg::Typed(pat) => { | ||
if let Pat::Ident(ident) = &*pat.pat { | ||
(&ident.ident, &pat.ty) | ||
} else { | ||
panic!("Only normal (ident pattern) parameters are supported, found {pat:?}",) | ||
} | ||
} | ||
FnArg::Receiver(_) => { | ||
panic!("#[native] attribute can only be used on freestanding functions!") | ||
} | ||
} | ||
}; | ||
|
||
let num_params = input.sig.inputs.len() - NUM_NON_ARG_PARAMS; | ||
|
||
// Generates names for use in `let` bindings. | ||
let arg_names = | ||
(0..num_params).map(|index| Ident::new(&format!("arg{index}"), Span::call_site())); | ||
|
||
// Generates `let argN = <arg_extraction_code>;` for each argument. | ||
// The separate 'let' bindings allow us to use `activation` in `<arg_extraction_code>` | ||
// without causing a borrowcheck error - otherwise, we could just put the extraction code | ||
// in the call expression.on (e.g. `user_fn) | ||
let arg_extractions = input.sig.inputs.iter().skip(NUM_NON_ARG_PARAMS).enumerate().zip(arg_names.clone()).map(|((index, arg), arg_name)| { | ||
let (param_ident, param_ty) = get_param_ident_ty(arg); | ||
|
||
let param_name = param_ident.to_string(); | ||
// Only use location information from `param_ident`. This ensures that | ||
// tokens emitted using `param_span` will be able to access local variables | ||
// defined by this macro, even if this macro is used from within another macro. | ||
let param_ty_span = param_ty.span().resolved_at(Span::call_site()); | ||
let param_ty_name = param_ty.to_token_stream().to_string(); | ||
|
||
// Subtle: We build up this error message in two parts. | ||
// For a native method `fn my_method(activation, this, my_argument: bool)`, we would produce the following string: | ||
// "my_method: Argument extraction failed for parameter `my_argument`: argument {:?} is not a `bool`". | ||
// Note that this string contains a `{:?}` format specifier - this will be substituted at *runtime*, when we have | ||
// the actual parameter value. | ||
let error_message_fmt = format!("{method_name}: Argument extraction failed for parameter `{param_name}`: argument {{:?}} is not a `{param_ty_name}`"); | ||
|
||
// Use `quote_spanned` to make compile-time error messages point at the argument in the function definition. | ||
quote_spanned! { param_ty_span=> | ||
let #arg_name = if let Some(arg) = crate::avm2::extract::ExtractFromVm::extract_from(&args[#index], activation) { | ||
arg | ||
} else { | ||
// As described above, `error_message_fmt` contains a `{:?}` for the actual argument value. | ||
// Substitute it in with `format!` | ||
return Err(format!(#error_message_fmt, args[#index]).into()) | ||
}; | ||
} | ||
}); | ||
|
||
let receiver_ty = get_param_ident_ty( | ||
input | ||
.sig | ||
.inputs | ||
.iter() | ||
.nth(1) | ||
.expect("Missing 'this' parameter"), | ||
) | ||
.1; | ||
|
||
let reciever_ty_span = receiver_ty.span(); | ||
|
||
let receiver_ty_name = receiver_ty.to_token_stream().to_string(); | ||
|
||
// These format strings are handled in the same way as `error_message_fmt` above. | ||
let error_message_fmt = format!( | ||
"{method_name}: Receiver extraction failed: receiver {{:?}} is not a `{receiver_ty_name}`" | ||
); | ||
let mismatched_arg_error_fmt = | ||
format!("{method_name}: Expected {num_params} arguments, found {{:?}}"); | ||
|
||
let receiver_extraction = quote_spanned! { reciever_ty_span=> | ||
// Try to extract the requested reciever type. | ||
let this = this.map(Value::Object); | ||
let this = if let Some(this) = crate::avm2::extract::ReceiverHelper::extract_from(&this, activation) { | ||
this | ||
} else { | ||
return Err(format!(#error_message_fmt, this).into()); | ||
}; | ||
}; | ||
|
||
let output = quote! { | ||
// Generate a method with the proper `NativeMethodImpl` signature | ||
#vis fn #method_name<'gc>( | ||
activation: &mut crate::avm2::Activation<'_, 'gc>, | ||
this: Option<crate::avm2::Object<'gc>>, | ||
args: &[crate::avm2::Value<'gc>] | ||
) -> Result<crate::avm2::Value<'gc>, crate::avm2::Error<'gc>> { | ||
// Paste the entire function definition provided by the user. | ||
// It will only be accessible here, so other code will only see | ||
// our generated function. | ||
#input; | ||
|
||
// Generate an error if called with the wrong number of parameters | ||
if args.len() != #num_params { | ||
return Err(format!(#mismatched_arg_error_fmt, args.len()).into()); | ||
} | ||
|
||
// Emit `let this = <receiver_extraction_code>;` for the receiver | ||
#receiver_extraction | ||
|
||
// Emit `let argN = <arg_extraction_code>;` for each argument. | ||
#(#arg_extractions)* | ||
|
||
// Finally, call the user's method with the extracted receiver and arguments. | ||
#method_name ( | ||
activation, this, #({ #arg_names } ),* | ||
) | ||
} | ||
}; | ||
output.into() | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
use crate::avm2::events::Event; | ||
use crate::avm2::object::TObject as _; | ||
use crate::avm2::Activation; | ||
use crate::avm2::Object; | ||
use crate::avm2::Value; | ||
use crate::display_object::DisplayObject; | ||
use crate::string::AvmString; | ||
use std::cell::{Ref, RefMut}; | ||
|
||
/// This trait is implemented for each type that can appear in the signature | ||
/// of a method annotated with `#[native]` (e.g. `bool`, `f64`, `Object`) | ||
pub trait ExtractFromVm<'a, 'gc>: Sized { | ||
/// Attempts to extract `Self` from the provided `Value`. | ||
/// If the extraction cannot be performed (or would require a coercion), | ||
/// then this should return `None`. | ||
/// | ||
/// The provided `activation` should only be used for debugging, | ||
/// or calling a method that requires a `MutationContext`. | ||
/// Any coercions (e.g. `coerce_to_string`) should have already been | ||
/// performed by the time this method is called. | ||
fn extract_from(val: &'a Value<'gc>, activation: &mut Activation<'_, 'gc>) -> Option<Self>; | ||
} | ||
|
||
/// Allows writing `arg: DisplayObject<'gc>` in a `#[native]` method | ||
impl<'a, 'gc> ExtractFromVm<'a, 'gc> for DisplayObject<'gc> { | ||
fn extract_from(val: &'a Value<'gc>, _activation: &mut Activation<'_, 'gc>) -> Option<Self> { | ||
val.as_object().and_then(|o| o.as_display_object()) | ||
} | ||
} | ||
|
||
/// Allows writing `arg: AvmString<'gc>` in a `#[native]` method | ||
impl<'a, 'gc> ExtractFromVm<'a, 'gc> for AvmString<'gc> { | ||
fn extract_from(val: &'a Value<'gc>, _activation: &mut Activation<'_, 'gc>) -> Option<Self> { | ||
if let Value::String(string) = val { | ||
Some(*string) | ||
} else { | ||
None | ||
} | ||
} | ||
} | ||
|
||
/// Allows writing `arg: f64` in a `#[native]` method | ||
impl<'a, 'gc> ExtractFromVm<'a, 'gc> for f64 { | ||
fn extract_from(val: &'a Value<'gc>, _activation: &mut Activation<'_, 'gc>) -> Option<Self> { | ||
if let Value::Number(num) = val { | ||
Some(*num) | ||
} else { | ||
None | ||
} | ||
} | ||
} | ||
|
||
/// Allows writing `arg: bool` in a `#[native]` method | ||
impl<'a, 'gc> ExtractFromVm<'a, 'gc> for bool { | ||
fn extract_from(val: &'a Value<'gc>, _activation: &mut Activation<'_, 'gc>) -> Option<Self> { | ||
if let Value::Bool(val) = val { | ||
Some(*val) | ||
} else { | ||
None | ||
} | ||
} | ||
} | ||
|
||
/// Allows writing `arg: Ref<'_, Event<'gc>>` in a `#[native]` method. | ||
/// This is a little more cumbersome for the user than allowing `&Event<'gc>`, | ||
/// but it avoids complicating the implementation. | ||
impl<'a, 'gc> ExtractFromVm<'a, 'gc> for Ref<'a, Event<'gc>> { | ||
fn extract_from( | ||
val: &'a Value<'gc>, | ||
_activation: &mut Activation<'_, 'gc>, | ||
) -> Option<Ref<'a, Event<'gc>>> { | ||
val.as_object_ref().and_then(|obj| obj.as_event()) | ||
} | ||
} | ||
|
||
/// Allows writing `arg: RefMut<'_, Event<'gc>>` in a `#[native]` method. | ||
/// This is a little more cumbersome for the user than allowing `&Event<'gc>`, | ||
/// but it avoids complicating the implementation. | ||
impl<'a, 'gc> ExtractFromVm<'a, 'gc> for RefMut<'a, Event<'gc>> { | ||
fn extract_from( | ||
val: &'a Value<'gc>, | ||
activation: &mut Activation<'_, 'gc>, | ||
) -> Option<RefMut<'a, Event<'gc>>> { | ||
val.as_object_ref() | ||
.and_then(|obj| obj.as_event_mut(activation.context.gc_context)) | ||
} | ||
} | ||
|
||
/// Allows writing `arg: Object<'gc>` in a `#[native]` method | ||
impl<'a, 'gc> ExtractFromVm<'a, 'gc> for Object<'gc> { | ||
fn extract_from( | ||
val: &'a Value<'gc>, | ||
_activation: &mut Activation<'_, 'gc>, | ||
) -> Option<Object<'gc>> { | ||
val.as_object() | ||
} | ||
} | ||
|
||
/// A helper trait to allow using both `Option<SomeNativeType>` and `SomeNativeType` | ||
/// as the receiver (`this`) argument of a `#[native]` method. | ||
pub trait ReceiverHelper<'a, 'gc>: Sized { | ||
// We take an `&Option<Value>` instead of a `Option<Object>` so that we can call | ||
// an `ExtractFromVm` impl without lifetime issues (it's impossible to turn a | ||
// `&'a Object` to an `&'a Value::Object(object)`). | ||
fn extract_from( | ||
val: &'a Option<Value<'gc>>, | ||
activation: &mut Activation<'_, 'gc>, | ||
) -> Option<Self>; | ||
} | ||
|
||
/// Allows writing `this: SomeNativeType` in a `#[native]` method, where `SomeNativeType` | ||
/// is any type with an `ExtractFromVm` (that is, it can be used as `arg: SomeNativeType`). | ||
/// If the function is called without a receiver (e.g. `Option<Object<'gc>>` is `None`), | ||
/// the extraction fails. | ||
impl<'a, 'gc, T> ReceiverHelper<'a, 'gc> for T | ||
where | ||
T: ExtractFromVm<'a, 'gc>, | ||
{ | ||
fn extract_from( | ||
val: &'a Option<Value<'gc>>, | ||
activation: &mut Activation<'_, 'gc>, | ||
) -> Option<Self> { | ||
if let Some(val) = val { | ||
let extracted: Option<T> = ExtractFromVm::extract_from(val, activation); | ||
extracted | ||
} else { | ||
None | ||
} | ||
} | ||
} | ||
|
||
/// Allows writing `this: Option<SomeNativeType>` in a `#[native]` method, where `SomeNativeType` | ||
/// is any type with an `ExtractFromVm` (that is, it can be used as `arg: SomeNativeType`). | ||
/// If the function is called without a receiver (e.g. `Option<Object<'gc>>` is `None`), | ||
/// then the `#[native]` function will be called with `None`. | ||
impl<'a, 'gc, T> ReceiverHelper<'a, 'gc> for Option<T> | ||
where | ||
T: ExtractFromVm<'a, 'gc>, | ||
{ | ||
fn extract_from( | ||
val: &'a Option<Value<'gc>>, | ||
activation: &mut Activation<'_, 'gc>, | ||
) -> Option<Self> { | ||
if let Some(val) = val { | ||
// If the function was called with a receiver, then try to extract | ||
// a value. | ||
let extracted: Option<T> = ExtractFromVm::extract_from(val, activation); | ||
// If the extraction failed, then treat this extraction as having failed | ||
// as well. For example, if the user writes `this: Option<DisplayObject>`, | ||
// and the function is called with a Boolean receiver (e.g. `true`), then | ||
// we want an error to be produced. We do *not* want to call the user's | ||
// function with `None` (since an invalid receiver is different from no | ||
// receiver at all). | ||
extracted.map(Some) | ||
} else { | ||
// If there's no receiver, then the extraction succeeds (the outer `Some`), | ||
// and we want to call the `#[native]` method with a value of `None` for `this`. | ||
Some(None) | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.