Sunday, May 17, 2009

Modifying an AS3 class with AbcExplorationLib

Recently, I made a couple of changes to AbcExplorationLib to allow the modification of a compiled ActionScript 3 (AS3) class.

The following code will be used to illustrate this feature:


class Shape {
public function foo() {


print("Base foo");
}
public function paint():void {
foo();
}
}

class Rectangle extends Shape{
public override function paint():void {
super.paint();
print( "Rectangle");
}
}

class Circle extends Shape{
public override function paint():void {
super.paint();
print( "Circle");
}
}

var shapes = [new Rectangle(),new Circle()];

for each(var s:Shape in shapes) {
s.paint();
}



Compiling this program using the Flex SDK and running it using the Tamarin binaries shows the following output:


$ java -jar asc.jar -import builtin.abc Shapes.as

Shapes.abc, 678 bytes written
$ avmshell Shapes.abc
Base foo
Rectangle
Base foo
Circle


Say that we want to create a definition of the foo method in the Rectangle class that overrides the definition from Shape.

1. First we load the class file:


let abcFile = using (new FileStream(sourceFile,FileMode.Open)) (
fun stream -> AvmAbcFile.Create(stream))


2. Then we need a definition for the new foo implementation. The following function creates a foo override that prints a message to the screen:


let newFooMethod(message:string) =
AvmMemberMethod(
CQualifiedName(Ns("",NamespaceKind.PackageNamespace),"foo"),
AvmMethod(
"",
SQualifiedName("*"),
[||],
Some <|
AvmMethodBody(
2,1,4,5,
[|
GetLocal0;
PushScope;
FindPropertyStrict(
MQualifiedName(
[|Ns("",
NamespaceKind.PackageNamespace)|],
"print"));
PushString message;
CallProperty(
MQualifiedName(
[|Ns("",
NamespaceKind.PackageNamespace)|],
"print"),
1);
Pop;
ReturnVoid
|],[||],[||]
)
),
AbcTraitAttribute.Override
)


3. We need a function to add a method to a existing class. Notice that the modification consists in only creating a new instance of the AvmClass with the same values as the original but adding the new method.


let addClassMethod(aClass,newMethod) =
match aClass with
| AvmClass(name,
superclassname,
init,
cinit,
slots,
methods,
pns) ->
AvmClass(name,
superclassname,
init,cinit,
slots,
newMethod::methods,
pns)



4. The following method is used to locate the rectangle class and apply the modification:


let modifyFileToAddMethod(f:AvmAbcFile) =
AvmAbcFile(f.Scripts,
f.Classes |>
List.map (fun (c:AvmClass) ->

match c.Name with
| CQualifiedName(_,"Rectangle") ->
addClassMethod(
c,
newFooMethod("New foo for Rectange!!!"))
| _ -> c))



5. Finally we write the input file back to disk:


let modifiedAbcFile = modifyFileToAddMethod(abcFile)
let abcFileCreator = AbcFileCreator()
let file = modifiedAbcFile.ToLowerIr(abcFileCreator)
using (new BinaryWriter(new FileStream(targetFileName,FileMode.Create)))
(fun f -> file.WriteTo(f))


After running this program we can execute the bytecode again to get the new results:


$ mono modify.exe Shapes.abc
...
$ avmshell Shapes_t.abc
New foo for Rectange!!!
Rectangle
Base foo
Circle


One area that really needs works is name handling. A particular challenge is to find a good way to represent multinames ( name references in a set of name spaces ).

In general the way of defining a method from scratch(for example in newFooMethod) needs some work since it is requires lots of details that might not be interesting for the developer.

Finally, another area that really needs improvement is the output file generation. Right now it requires the user to write three instructions to write the file to disc. This will be changed to be similar to the load process.


Code for this program can be found as part of the AbcExplorationLib samples.