avm2: Include class name in ScriptObject debug (#7512)

* avm2: Include class name in ScriptObject debug

Currently, the `ScriptObject` debug impl is almost useless -
while you determine if two printed objects are the same
by comparing the pointer value, you'll have no idea what
kind of object it actually is.

This PR now formats the `ScriptObject` output as a struct,
printing a (fake) "class" field containing the class name.

Before/after:

```
[ERROR ruffle_core::avm2::activation] AVM2 error: Cannot coerce Object(ScriptObject(ScriptObject(GcCell(Gc { ptr: 0x55f863936db8 })))) to an QName { ns: Private("Test.as$38"), name: "Second" }
[ERROR ruffle_core::avm2::activation] AVM2 error: Cannot coerce Object(ScriptObject(ScriptObject { class: "Object", ptr: 0x55ee0ad161e0 })) to an QName { ns: Private("Test.as$38"), name: "Second" }
```

Getting access to the class name from a `Debug` impl is tricky:

Developers can (and should be able to) insert logging statements
whereever they want, so any `GcCell` may be mutably borrowed.
Panics in debug impls are extremely frustrating to deal with,
so I've ensured that we only use `try_borrow` at each step.
If any of the attempted borrows fail, we print out an error message
in the "class_name" field, but we're still able to print the
rest of the `ScriptObject`.

Additionally, we have no access to a `MutationContext`, so we
cannot allocate a new `AvmString`. To get around this,
I've created a new method `QName::to_qualified_name_no_mc`,
which uses an `Either` to return a `WString` instead of allocating
an `AvmString`. This is more cumbersome to work with than the
nrmal `QName::to_qualified_name`, so we'll only want to use
it when we have no other choice.
This commit is contained in:
Aaron Hill 2022-08-05 00:13:00 -05:00 committed by GitHub
parent 2383e6850f
commit 2f8dde86af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 84 additions and 9 deletions

View File

@ -4,8 +4,10 @@ use crate::avm2::activation::Activation;
use crate::avm2::script::TranslationUnit; use crate::avm2::script::TranslationUnit;
use crate::avm2::value::Value; use crate::avm2::value::Value;
use crate::avm2::Error; use crate::avm2::Error;
use crate::either::Either;
use crate::string::{AvmString, WStr, WString}; use crate::string::{AvmString, WStr, WString};
use gc_arena::{Collect, MutationContext}; use gc_arena::{Collect, MutationContext};
use std::fmt::Debug;
use swf::avm2::types::{ use swf::avm2::types::{
AbcFile, Index, Multiname as AbcMultiname, Namespace as AbcNamespace, AbcFile, Index, Multiname as AbcMultiname, Namespace as AbcNamespace,
NamespaceSet as AbcNamespaceSet, NamespaceSet as AbcNamespaceSet,
@ -135,7 +137,7 @@ impl<'gc> Namespace<'gc> {
/// `QName`. All other forms of names and multinames are either versions of /// `QName`. All other forms of names and multinames are either versions of
/// `QName` with unspecified parameters, or multiple names to be checked in /// `QName` with unspecified parameters, or multiple names to be checked in
/// order. /// order.
#[derive(Clone, Copy, Collect, Debug, Hash)] #[derive(Clone, Copy, Collect, Hash)]
#[collect(no_drop)] #[collect(no_drop)]
pub struct QName<'gc> { pub struct QName<'gc> {
ns: Namespace<'gc>, ns: Namespace<'gc>,
@ -222,15 +224,33 @@ impl<'gc> QName<'gc> {
/// Converts this `QName` to a fully qualified name. /// Converts this `QName` to a fully qualified name.
pub fn to_qualified_name(self, mc: MutationContext<'gc, '_>) -> AvmString<'gc> { pub fn to_qualified_name(self, mc: MutationContext<'gc, '_>) -> AvmString<'gc> {
match self.to_qualified_name_no_mc() {
Either::Left(avm_string) => avm_string,
Either::Right(wstring) => AvmString::new(mc, wstring),
}
}
/// Like `to_qualified_name`, but avoids the need for a `MutationContext`
/// by returning `Either::Right(wstring)` when it would otherwise
/// be necessary to allocate a new `AvmString`.
///
/// This method is intended for contexts like `Debug` impls where
/// a `MutationContext` is not available. Normally, you should
/// use `to_qualified_name`
pub fn to_qualified_name_no_mc(self) -> Either<AvmString<'gc>, WString> {
let uri = self.namespace().as_uri(); let uri = self.namespace().as_uri();
let name = self.local_name(); let name = self.local_name();
uri.is_empty().then_some(name).unwrap_or_else(|| { if uri.is_empty() {
Either::Left(name)
} else {
Either::Right({
let mut buf = WString::from(uri.as_wstr()); let mut buf = WString::from(uri.as_wstr());
buf.push_str(WStr::from_units(b"::")); buf.push_str(WStr::from_units(b"::"));
buf.push_str(&name); buf.push_str(&name);
AvmString::new(mc, buf) buf
}) })
} }
}
pub fn local_name(&self) -> AvmString<'gc> { pub fn local_name(&self) -> AvmString<'gc> {
self.name self.name
@ -264,6 +284,15 @@ impl<'gc> QName<'gc> {
} }
} }
impl<'gc> Debug for QName<'gc> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
match self.to_qualified_name_no_mc() {
Either::Left(name) => write!(f, "{}", name),
Either::Right(name) => write!(f, "{}", name),
}
}
}
/// A `Multiname` consists of a name which could be resolved in one or more /// A `Multiname` consists of a name which could be resolved in one or more
/// potential namespaces. /// potential namespaces.
/// ///

View File

@ -17,7 +17,7 @@ use crate::avm2::TranslationUnit;
use crate::string::AvmString; use crate::string::AvmString;
use fnv::FnvHashMap; use fnv::FnvHashMap;
use gc_arena::{Collect, GcCell, MutationContext}; use gc_arena::{Collect, GcCell, MutationContext};
use std::cell::{Ref, RefMut}; use std::cell::{BorrowError, Ref, RefMut};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
/// An Object which can be called to execute its function code. /// An Object which can be called to execute its function code.
@ -706,6 +706,15 @@ impl<'gc> ClassObject<'gc> {
self.0.read().class_vtable self.0.read().class_vtable
} }
/// Like `inner_class_definition`, but returns an `Err(BorrowError)` instead of panicking
/// if our `GcCell` is already mutably borrowed. This is useful
/// in contexts where panicking would be extremely undesirable,
/// and there's a fallback if we cannot obtain the `Class`
/// (such as `Debug` impls),
pub fn try_inner_class_definition(&self) -> Result<GcCell<'gc, Class<'gc>>, BorrowError> {
self.0.try_read().map(|c| c.class)
}
pub fn inner_class_definition(self) -> GcCell<'gc, Class<'gc>> { pub fn inner_class_definition(self) -> GcCell<'gc, Class<'gc>> {
self.0.read().class self.0.read().class
} }

View File

@ -24,7 +24,7 @@ pub fn scriptobject_allocator<'gc>(
} }
/// Default implementation of `avm2::Object`. /// Default implementation of `avm2::Object`.
#[derive(Clone, Collect, Debug, Copy)] #[derive(Clone, Collect, Copy)]
#[collect(no_drop)] #[collect(no_drop)]
pub struct ScriptObject<'gc>(GcCell<'gc, ScriptObjectData<'gc>>); pub struct ScriptObject<'gc>(GcCell<'gc, ScriptObjectData<'gc>>);
@ -429,3 +429,35 @@ impl<'gc> ScriptObjectData<'gc> {
self.vtable = Some(vtable); self.vtable = Some(vtable);
} }
} }
impl<'gc> Debug for ScriptObject<'gc> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
let mut fmt = f.debug_struct("ScriptObject");
let class_name = self
.0
.try_read()
.map(|obj| obj.instance_of())
.transpose()
.map(|class_obj| {
class_obj
.and_then(|class_obj| class_obj.try_inner_class_definition())
.and_then(|class| class.try_read().map(|c| c.name()))
});
match class_name {
Some(Ok(class_name)) => {
fmt.field("class", &class_name);
}
Some(Err(err)) => {
fmt.field("class", &err);
}
None => {
fmt.field("class", &"<None>");
}
}
fmt.field("ptr", &self.0.as_ptr());
fmt.finish()
}
}

4
core/src/either.rs Normal file
View File

@ -0,0 +1,4 @@
pub enum Either<A, B> {
Left(A),
Right(B),
}

View File

@ -23,6 +23,7 @@ pub mod context;
pub mod context_menu; pub mod context_menu;
mod drawing; mod drawing;
mod ecma_conversions; mod ecma_conversions;
pub(crate) mod either;
pub mod events; pub mod events;
pub mod focus_tracker; pub mod focus_tracker;
mod font; mod font;