Skip to content

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
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions core/macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ version.workspace = true
proc-macro = true

[dependencies]
proc-macro2 = "1.0.43"
quote = "1.0.23"
syn = { version = "1.0.107", features = ["full"] }
51 changes: 50 additions & 1 deletion core/macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
//! Proc macros used by Ruffle to generate various boilerplate.
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse_macro_input, parse_quote, FnArg, ImplItem, ImplItemMethod, ItemEnum, ItemTrait, Pat,
TraitItem, Visibility,
};

mod native;

/// `enum_trait_object` will define an enum whose variants each implement a trait.
/// It can be used as faux-dynamic dispatch. This is used as an alternative to a
/// trait object, which doesn't get along with GC'd types.
Expand Down Expand Up @@ -149,3 +150,51 @@ pub fn enum_trait_object(args: TokenStream, item: TokenStream) -> TokenStream {

out.into()
}

/// The `native` attribute allows you to implement an ActionScript
/// impl using native Rust/ruffle types, rather than taking in a `&[Value<'gc>]`
/// of arguments.
///
/// For example, consider the following native function in 'Event.as':
///
/// ```text
/// private native function init(type:String, bubbles:Boolean = false, cancelable:Boolean = false):void;
/// ```
///
/// Using the `#\[native\] macro, we can implement it like this:
///
/// ```rust,compile_fail
/// #[native]
///pub fn init<'gc>(
/// _activation: &mut Activation<'_, 'gc, '_>,
/// mut this: RefMut<'_, Event<'gc>>,
/// event_type: AvmString<'gc>,
/// bubbles: bool,
/// cancelable: bool
///) -> Result<Value<'gc>, Error<'gc>> {
/// this.set_event_type(event_type);
/// this.set_bubbles(bubbles);
/// this.set_cancelable(cancelable);
/// Ok(Value::Undefined)
///}
///```
///
/// We specify the desired types for `this`, along with each paramter.
/// The `#[native]` macro will generate a function with the normal `NativeMethodImpl`,
/// which will check that all of the arguments have the expected type, and call your function.
/// If the receiver (`this`) or any of the argument `Value`s do *not* match your declared types,
/// the generated function will return an error without calling your function.
///
/// To see all supported argument types, look at the `ExtractFromVm` impls in `core::avm2::extract`
///
/// Note: This macro **does not** perform *coercions* (e.g. `coerce_to_object`, `coerce_to_number`).
/// Instead, it checks that the receiver/argument is *already* of the correct type (e.g. `Value::Object` or `Value::Number`).
///
/// The actual coercion logic should have been already performed by Ruffle by the time the generated method is called:
/// * For native methods, this is done by `resolve_parameter` using the type signature defined in the loaded bytecode
/// * For methods defined entirely in Rust, you must define the method using `Method::from_builtin_and_params`.
/// Using a native method is much easier.
#[proc_macro_attribute]
pub fn native(args: TokenStream, item: TokenStream) -> TokenStream {
native::native_impl(args, item)
}
133 changes: 133 additions & 0 deletions core/macros/src/native.rs
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()
}
1 change: 1 addition & 0 deletions core/src/avm2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ mod class;
mod domain;
pub mod error;
mod events;
mod extract;
mod function;
pub mod globals;
mod method;
Expand Down
161 changes: 161 additions & 0 deletions core/src/avm2/extract.rs
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)
}
}
}
Loading