Skip to content

Commit 6f3c682

Browse files
Aaron1011Dinnerbone
authored andcommitted
avm2: Add #[native] macro to allow defining methods with native types
This allows you to define a native method like: ```rust fn my_method(activation: &mut Activation, this: DisplayObject<'gc>, arg: bool) ``` instead of needing to use `Option<Object<'gc>>` and `&[Value<'gc>]` and manual coercions. See the doc comments for more details.
1 parent 133044b commit 6f3c682

File tree

9 files changed

+402
-91
lines changed

9 files changed

+402
-91
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/macros/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ version.workspace = true
1111
proc-macro = true
1212

1313
[dependencies]
14+
proc-macro2 = "1.0.43"
1415
quote = "1.0.23"
1516
syn = { version = "1.0.107", features = ["full"] }

core/macros/src/lib.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
//! Proc macros used by Ruffle to generate various boilerplate.
2-
extern crate proc_macro;
32
use proc_macro::TokenStream;
43
use quote::quote;
54
use syn::{
65
parse_macro_input, parse_quote, FnArg, ImplItem, ImplItemMethod, ItemEnum, ItemTrait, Pat,
76
TraitItem, Visibility,
87
};
98

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

150151
out.into()
151152
}
153+
154+
/// The `native` attribute allows you to implement an ActionScript
155+
/// impl using native Rust/ruffle types, rather than taking in a `&[Value<'gc>]`
156+
/// of arguments.
157+
///
158+
/// For example, consider the following native function in 'Event.as':
159+
///
160+
/// ```text
161+
/// private native function init(type:String, bubbles:Boolean = false, cancelable:Boolean = false):void;
162+
/// ```
163+
///
164+
/// Using the `#\[native\] macro, we can implement it like this:
165+
///
166+
/// ```rust,compile_fail
167+
/// #[native]
168+
///pub fn init<'gc>(
169+
/// _activation: &mut Activation<'_, 'gc, '_>,
170+
/// mut this: RefMut<'_, Event<'gc>>,
171+
/// event_type: AvmString<'gc>,
172+
/// bubbles: bool,
173+
/// cancelable: bool
174+
///) -> Result<Value<'gc>, Error<'gc>> {
175+
/// this.set_event_type(event_type);
176+
/// this.set_bubbles(bubbles);
177+
/// this.set_cancelable(cancelable);
178+
/// Ok(Value::Undefined)
179+
///}
180+
///```
181+
///
182+
/// We specify the desired types for `this`, along with each paramter.
183+
/// The `#[native]` macro will generate a function with the normal `NativeMethodImpl`,
184+
/// which will check that all of the arguments have the expected type, and call your function.
185+
/// If the receiver (`this`) or any of the argument `Value`s do *not* match your declared types,
186+
/// the generated function will return an error without calling your function.
187+
///
188+
/// To see all supported argument types, look at the `ExtractFromVm` impls in `core::avm2::extract`
189+
///
190+
/// Note: This macro **does not** perform *coercions* (e.g. `coerce_to_object`, `coerce_to_number`).
191+
/// Instead, it checks that the receiver/argument is *already* of the correct type (e.g. `Value::Object` or `Value::Number`).
192+
///
193+
/// The actual coercion logic should have been already performed by Ruffle by the time the generated method is called:
194+
/// * For native methods, this is done by `resolve_parameter` using the type signature defined in the loaded bytecode
195+
/// * For methods defined entirely in Rust, you must define the method using `Method::from_builtin_and_params`.
196+
/// Using a native method is much easier.
197+
#[proc_macro_attribute]
198+
pub fn native(args: TokenStream, item: TokenStream) -> TokenStream {
199+
native::native_impl(args, item)
200+
}

core/macros/src/native.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
use proc_macro::TokenStream;
2+
use proc_macro2::{Ident, Span};
3+
use quote::{quote, quote_spanned, ToTokens};
4+
use syn::spanned::Spanned;
5+
use syn::{parse_macro_input, FnArg, ItemFn, Pat};
6+
7+
// A `#[native]` method is of the form `fn(&mut Activation, ReceiverType, Arg0Type, Arg1Type, ...)
8+
// When looking for arguments, we skip over the first 2 parameters.
9+
const NUM_NON_ARG_PARAMS: usize = 2;
10+
11+
pub fn native_impl(_args: TokenStream, item: TokenStream) -> TokenStream {
12+
let input = parse_macro_input!(item as ItemFn);
13+
let vis = &input.vis;
14+
let method_name = &input.sig.ident;
15+
16+
// Extracts `(name, Type)` from a parameter definition of the form `name: Type`
17+
let get_param_ident_ty = |arg| {
18+
let arg: &FnArg = arg;
19+
match arg {
20+
FnArg::Typed(pat) => {
21+
if let Pat::Ident(ident) = &*pat.pat {
22+
(&ident.ident, &pat.ty)
23+
} else {
24+
panic!("Only normal (ident pattern) parameters are supported, found {pat:?}",)
25+
}
26+
}
27+
FnArg::Receiver(_) => {
28+
panic!("#[native] attribute can only be used on freestanding functions!")
29+
}
30+
}
31+
};
32+
33+
let num_params = input.sig.inputs.len() - NUM_NON_ARG_PARAMS;
34+
35+
// Generates names for use in `let` bindings.
36+
let arg_names =
37+
(0..num_params).map(|index| Ident::new(&format!("arg{index}"), Span::call_site()));
38+
39+
// Generates `let argN = <arg_extraction_code>;` for each argument.
40+
// The separate 'let' bindings allow us to use `activation` in `<arg_extraction_code>`
41+
// without causing a borrowcheck error - otherwise, we could just put the extraction code
42+
// in the call expression.on (e.g. `user_fn)
43+
let arg_extractions = input.sig.inputs.iter().skip(NUM_NON_ARG_PARAMS).enumerate().zip(arg_names.clone()).map(|((index, arg), arg_name)| {
44+
let (param_ident, param_ty) = get_param_ident_ty(arg);
45+
46+
let param_name = param_ident.to_string();
47+
// Only use location information from `param_ident`. This ensures that
48+
// tokens emitted using `param_span` will be able to access local variables
49+
// defined by this macro, even if this macro is used from within another macro.
50+
let param_ty_span = param_ty.span().resolved_at(Span::call_site());
51+
let param_ty_name = param_ty.to_token_stream().to_string();
52+
53+
// Subtle: We build up this error message in two parts.
54+
// For a native method `fn my_method(activation, this, my_argument: bool)`, we would produce the following string:
55+
// "my_method: Argument extraction failed for parameter `my_argument`: argument {:?} is not a `bool`".
56+
// Note that this string contains a `{:?}` format specifier - this will be substituted at *runtime*, when we have
57+
// the actual parameter value.
58+
let error_message_fmt = format!("{method_name}: Argument extraction failed for parameter `{param_name}`: argument {{:?}} is not a `{param_ty_name}`");
59+
60+
// Use `quote_spanned` to make compile-time error messages point at the argument in the function definition.
61+
quote_spanned! { param_ty_span=>
62+
let #arg_name = if let Some(arg) = crate::avm2::extract::ExtractFromVm::extract_from(&args[#index], activation) {
63+
arg
64+
} else {
65+
// As described above, `error_message_fmt` contains a `{:?}` for the actual argument value.
66+
// Substitute it in with `format!`
67+
return Err(format!(#error_message_fmt, args[#index]).into())
68+
};
69+
}
70+
});
71+
72+
let receiver_ty = get_param_ident_ty(
73+
input
74+
.sig
75+
.inputs
76+
.iter()
77+
.nth(1)
78+
.expect("Missing 'this' parameter"),
79+
)
80+
.1;
81+
82+
let reciever_ty_span = receiver_ty.span();
83+
84+
let receiver_ty_name = receiver_ty.to_token_stream().to_string();
85+
86+
// These format strings are handled in the same way as `error_message_fmt` above.
87+
let error_message_fmt = format!(
88+
"{method_name}: Receiver extraction failed: receiver {{:?}} is not a `{receiver_ty_name}`"
89+
);
90+
let mismatched_arg_error_fmt =
91+
format!("{method_name}: Expected {num_params} arguments, found {{:?}}");
92+
93+
let receiver_extraction = quote_spanned! { reciever_ty_span=>
94+
// Try to extract the requested reciever type.
95+
let this = this.map(Value::Object);
96+
let this = if let Some(this) = crate::avm2::extract::ReceiverHelper::extract_from(&this, activation) {
97+
this
98+
} else {
99+
return Err(format!(#error_message_fmt, this).into());
100+
};
101+
};
102+
103+
let output = quote! {
104+
// Generate a method with the proper `NativeMethodImpl` signature
105+
#vis fn #method_name<'gc>(
106+
activation: &mut crate::avm2::Activation<'_, 'gc>,
107+
this: Option<crate::avm2::Object<'gc>>,
108+
args: &[crate::avm2::Value<'gc>]
109+
) -> Result<crate::avm2::Value<'gc>, crate::avm2::Error<'gc>> {
110+
// Paste the entire function definition provided by the user.
111+
// It will only be accessible here, so other code will only see
112+
// our generated function.
113+
#input;
114+
115+
// Generate an error if called with the wrong number of parameters
116+
if args.len() != #num_params {
117+
return Err(format!(#mismatched_arg_error_fmt, args.len()).into());
118+
}
119+
120+
// Emit `let this = <receiver_extraction_code>;` for the receiver
121+
#receiver_extraction
122+
123+
// Emit `let argN = <arg_extraction_code>;` for each argument.
124+
#(#arg_extractions)*
125+
126+
// Finally, call the user's method with the extracted receiver and arguments.
127+
#method_name (
128+
activation, this, #({ #arg_names } ),*
129+
)
130+
}
131+
};
132+
output.into()
133+
}

core/src/avm2.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ mod class;
3030
mod domain;
3131
pub mod error;
3232
mod events;
33+
mod extract;
3334
mod function;
3435
pub mod globals;
3536
mod method;

core/src/avm2/extract.rs

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
use crate::avm2::events::Event;
2+
use crate::avm2::object::TObject as _;
3+
use crate::avm2::Activation;
4+
use crate::avm2::Object;
5+
use crate::avm2::Value;
6+
use crate::display_object::DisplayObject;
7+
use crate::string::AvmString;
8+
use std::cell::{Ref, RefMut};
9+
10+
/// This trait is implemented for each type that can appear in the signature
11+
/// of a method annotated with `#[native]` (e.g. `bool`, `f64`, `Object`)
12+
pub trait ExtractFromVm<'a, 'gc>: Sized {
13+
/// Attempts to extract `Self` from the provided `Value`.
14+
/// If the extraction cannot be performed (or would require a coercion),
15+
/// then this should return `None`.
16+
///
17+
/// The provided `activation` should only be used for debugging,
18+
/// or calling a method that requires a `MutationContext`.
19+
/// Any coercions (e.g. `coerce_to_string`) should have already been
20+
/// performed by the time this method is called.
21+
fn extract_from(val: &'a Value<'gc>, activation: &mut Activation<'_, 'gc>) -> Option<Self>;
22+
}
23+
24+
/// Allows writing `arg: DisplayObject<'gc>` in a `#[native]` method
25+
impl<'a, 'gc> ExtractFromVm<'a, 'gc> for DisplayObject<'gc> {
26+
fn extract_from(val: &'a Value<'gc>, _activation: &mut Activation<'_, 'gc>) -> Option<Self> {
27+
val.as_object().and_then(|o| o.as_display_object())
28+
}
29+
}
30+
31+
/// Allows writing `arg: AvmString<'gc>` in a `#[native]` method
32+
impl<'a, 'gc> ExtractFromVm<'a, 'gc> for AvmString<'gc> {
33+
fn extract_from(val: &'a Value<'gc>, _activation: &mut Activation<'_, 'gc>) -> Option<Self> {
34+
if let Value::String(string) = val {
35+
Some(*string)
36+
} else {
37+
None
38+
}
39+
}
40+
}
41+
42+
/// Allows writing `arg: f64` in a `#[native]` method
43+
impl<'a, 'gc> ExtractFromVm<'a, 'gc> for f64 {
44+
fn extract_from(val: &'a Value<'gc>, _activation: &mut Activation<'_, 'gc>) -> Option<Self> {
45+
if let Value::Number(num) = val {
46+
Some(*num)
47+
} else {
48+
None
49+
}
50+
}
51+
}
52+
53+
/// Allows writing `arg: bool` in a `#[native]` method
54+
impl<'a, 'gc> ExtractFromVm<'a, 'gc> for bool {
55+
fn extract_from(val: &'a Value<'gc>, _activation: &mut Activation<'_, 'gc>) -> Option<Self> {
56+
if let Value::Bool(val) = val {
57+
Some(*val)
58+
} else {
59+
None
60+
}
61+
}
62+
}
63+
64+
/// Allows writing `arg: Ref<'_, Event<'gc>>` in a `#[native]` method.
65+
/// This is a little more cumbersome for the user than allowing `&Event<'gc>`,
66+
/// but it avoids complicating the implementation.
67+
impl<'a, 'gc> ExtractFromVm<'a, 'gc> for Ref<'a, Event<'gc>> {
68+
fn extract_from(
69+
val: &'a Value<'gc>,
70+
_activation: &mut Activation<'_, 'gc>,
71+
) -> Option<Ref<'a, Event<'gc>>> {
72+
val.as_object_ref().and_then(|obj| obj.as_event())
73+
}
74+
}
75+
76+
/// Allows writing `arg: RefMut<'_, Event<'gc>>` in a `#[native]` method.
77+
/// This is a little more cumbersome for the user than allowing `&Event<'gc>`,
78+
/// but it avoids complicating the implementation.
79+
impl<'a, 'gc> ExtractFromVm<'a, 'gc> for RefMut<'a, Event<'gc>> {
80+
fn extract_from(
81+
val: &'a Value<'gc>,
82+
activation: &mut Activation<'_, 'gc>,
83+
) -> Option<RefMut<'a, Event<'gc>>> {
84+
val.as_object_ref()
85+
.and_then(|obj| obj.as_event_mut(activation.context.gc_context))
86+
}
87+
}
88+
89+
/// Allows writing `arg: Object<'gc>` in a `#[native]` method
90+
impl<'a, 'gc> ExtractFromVm<'a, 'gc> for Object<'gc> {
91+
fn extract_from(
92+
val: &'a Value<'gc>,
93+
_activation: &mut Activation<'_, 'gc>,
94+
) -> Option<Object<'gc>> {
95+
val.as_object()
96+
}
97+
}
98+
99+
/// A helper trait to allow using both `Option<SomeNativeType>` and `SomeNativeType`
100+
/// as the receiver (`this`) argument of a `#[native]` method.
101+
pub trait ReceiverHelper<'a, 'gc>: Sized {
102+
// We take an `&Option<Value>` instead of a `Option<Object>` so that we can call
103+
// an `ExtractFromVm` impl without lifetime issues (it's impossible to turn a
104+
// `&'a Object` to an `&'a Value::Object(object)`).
105+
fn extract_from(
106+
val: &'a Option<Value<'gc>>,
107+
activation: &mut Activation<'_, 'gc>,
108+
) -> Option<Self>;
109+
}
110+
111+
/// Allows writing `this: SomeNativeType` in a `#[native]` method, where `SomeNativeType`
112+
/// is any type with an `ExtractFromVm` (that is, it can be used as `arg: SomeNativeType`).
113+
/// If the function is called without a receiver (e.g. `Option<Object<'gc>>` is `None`),
114+
/// the extraction fails.
115+
impl<'a, 'gc, T> ReceiverHelper<'a, 'gc> for T
116+
where
117+
T: ExtractFromVm<'a, 'gc>,
118+
{
119+
fn extract_from(
120+
val: &'a Option<Value<'gc>>,
121+
activation: &mut Activation<'_, 'gc>,
122+
) -> Option<Self> {
123+
if let Some(val) = val {
124+
let extracted: Option<T> = ExtractFromVm::extract_from(val, activation);
125+
extracted
126+
} else {
127+
None
128+
}
129+
}
130+
}
131+
132+
/// Allows writing `this: Option<SomeNativeType>` in a `#[native]` method, where `SomeNativeType`
133+
/// is any type with an `ExtractFromVm` (that is, it can be used as `arg: SomeNativeType`).
134+
/// If the function is called without a receiver (e.g. `Option<Object<'gc>>` is `None`),
135+
/// then the `#[native]` function will be called with `None`.
136+
impl<'a, 'gc, T> ReceiverHelper<'a, 'gc> for Option<T>
137+
where
138+
T: ExtractFromVm<'a, 'gc>,
139+
{
140+
fn extract_from(
141+
val: &'a Option<Value<'gc>>,
142+
activation: &mut Activation<'_, 'gc>,
143+
) -> Option<Self> {
144+
if let Some(val) = val {
145+
// If the function was called with a receiver, then try to extract
146+
// a value.
147+
let extracted: Option<T> = ExtractFromVm::extract_from(val, activation);
148+
// If the extraction failed, then treat this extraction as having failed
149+
// as well. For example, if the user writes `this: Option<DisplayObject>`,
150+
// and the function is called with a Boolean receiver (e.g. `true`), then
151+
// we want an error to be produced. We do *not* want to call the user's
152+
// function with `None` (since an invalid receiver is different from no
153+
// receiver at all).
154+
extracted.map(Some)
155+
} else {
156+
// If there's no receiver, then the extraction succeeds (the outer `Some`),
157+
// and we want to call the `#[native]` method with a value of `None` for `this`.
158+
Some(None)
159+
}
160+
}
161+
}

0 commit comments

Comments
 (0)