Introduction to the emit API

As3commons-bytecode offers an API that enables a developer to generate classes at run-time.
All the necessary classes for this functionality can be found in the org.as3commons.bytecode.emit.* package.
This API can form the basis for mocking or run-time proxy libraries.

Generating classes

The main entry point for run-time class generation is the IAbcBuilder interface.
The AbcBuilder class is an implementation of this interface and accepts an optional AbcBuilder instance for its constructor. When no AbcBuilder is passed in it will create a new AbcBuilder instance during its build process, otherwise the specified AbcBuilder will be used to add the generated classes and interfaces to.

Below is an example of a simple run-time class.
Before defining a class there first needs to be a package definition:

var abcBuilder:IAbcBuilder = new AbcBuilder();
var packageBuilder:IPackageBuilder = abcBuilder.definePackage("com.classes.generated");

Once the package has been defined, a class can be defined:

var classBuilder:IClassBuilder = packageBuilder.defineClass("RuntimeClass");

A super class can also be defined, for this a fully qualified name is required:

var classBuilder:IClassBuilder = packageBuilder.defineClass("RuntimeByteArraySubClass","flash.utils.ByteArray");

To have the generated class implement one or more interfaces use the IClassBuilder.implementInterface() method, the interface names are assumed to be fully qualified names:

classBuilder.implementInterface("flash.events.IEventDispatcher");
Note: Invoking implementInterface() does *not* automatically add the required methods to the class, this needs to be done manually.

Other properties that can be set on the IClassBuilder include:

  • isDynamic
  • isFinal

The actionscript source equivalent of the above statements would look like this:

package com.classes.generated {
  public class RuntimeByteArraySubClass extends ByteArray implements IEventDispatcher {
  }
}

Generating properties

Next step is defining properties on the class. Use the defineProperty() method on the IClassBuilder instance for this:

var propertyBuilder:IPropertyBuilder = classBuilder.defineProperty("name","String");

The first argument is the name of the property, the second is the type, this always needs to be a fully qualified name.
To define a default value for the property add a third argument:

var propertyBuilder:IPropertyBuilder = classBuilder.defineProperty("name","String","defaultName");

The actionscript source equivalent of those statements would look like this:

public var name:String = "defaultName";

Properties with complex types can have their initial values defined by setting the memberInitialization property to a valid MemberInitialization instance.
To simply have the class instantiated all that is needed is a new MemberInitialization instance:

propertyBuilder.memberInitialization = new MemberInitialization();

When the constructor needs one or more constructor arguments, add them using the constructorArguments property:

propertyBuilder.memberInitialization = new MemberInitialization();
propertyBuilder.memberInitialization.constructorArguments = ["myString", true, 100];
Attention: Only constructor arguments of a simple type are supported (i.e. String, Number, etc).

By default the generated property will be public, to change this use the IPropertyBuilder.visibility property:

propertyBuilder.visibility = MemberVisibility.PRIVATE;

To assign a custom namespace to the property, assign the specified namespace URL to the IPropertyBuilder.namespace property and the name to the IPropertyBuilder.scopeName property:

propertyBuilder.namespace = "http://www.somedomain.com/customnamespace";
propertyBuilder.scopeName = "my_custom_namespace";

This assumes that the namespace would have been declared like this:

public namespace my_custom_namespace = "http://www.somedomain.com/customnamespace";

Other properties that can be set on the IPropertyBuilder include:

  • isOverride
  • isFinal
  • isStatic
  • isConstant

Generating getters and setters

Getters and setters, also known as accessors, are added using the defineAccessor() method, its similar to the defineProperty() method:

var accessorBuilder:IAccessorBuilder = classBuilder.defineAccessor("count","int",100);

The actionscript source equivalent of this statement would look like this:

private var _count:int = 100;

public function get count():int {
  return _count;
}

public function set count(value:int):void {
  _count =  value;
}

By default the accessor will be read/write, this can be changed using the access property on the IAccessorBuilder interface:

accessorBuilder.access = AccessorAccess.READ_ONLY;

By default a private property will be generated with a name in the format "_" + accessorName.
This private property is used to store the actual value of the accessor.

To use a custom IPropertyBuilder for this create one and assign it to the IAccessorBuilder.property property:

accessorBuilder.property = new PropertyBuilder("count","int",1000);

Overriding accessor body creation

To completely override the creation of the getter and setter method bodies add listeners for the AccessorBuilderEvent.BUILD_GETTER and AccessorBuilderEvent.BUILD_SETTER events.
When either of these events are handled the IAccessorBuilder will delegate the creation of the IMethodBuilder instances to the event handlers assigned to these events.
In these event handlers logic can be placed to create a valid IMethodBuilder and its method body, after creation such an instance needs to be assigned to the AccessorBuilderEvent.builder property.

accessorBuilder.addEventLister(AccessorBuilderEvent.BUILD_GETTER, buildGetterHandler);
public function buildGetterHandler(event:AccessorBuilderEvent):void {
	var methodBuilder:IMethodBuilder = new MethodBuilder():
	//logic ommitted...
	event.builder = methodBuilder;
}

Visibility and namespace assignment is the same as for property generation.

Other properties that can be set on the IAccessorBuilder include:

  • isOverride
  • isFinal
  • isStatic

Generating methods

The IMethodBuilder interface supplies the required API to start adding methods to a generated class.

To receive an instance of this interface invoke the defineMethod() on the IClassBuilder instance:

var methodBuilder:IMethodBuilder = classBuilder.defineMethod("multiplyByHundred");

Setting visibility and namespaces on the method works the same way as for properties and accessors.

To define method arguments invoke the defineArgument() method:

var argument:MethodArgument = methodBuilder.defineArgument("int");

Further properties for an argument are isOptional and defaultValue, their names are self-explanatory.

By default a generated method has a return type of void, to change this set the returnType property:

methodBuilder.returnType = "int";

Defining method bodies

To add a method body a certain knowledge about AVM opcodes is required:

methodBuilder.addOpcode(Opcode.getlocal_0)
             .addOpcode(Opcode.pushscope)
             .addOpcode(Opcode.getlocal_1)
             .addOpcode(Opcode.pushint, [100])
             .addOpcode(Opcode.multiply)
             .addOpcode(Opcode.setlocal_1)
             .addOpcode(Opcode.getlocal_1)
             .addOpcode(Opcode.returnvalue);

For more information on AVM instructions follow this link: AVM2 overview.

The actionscript source equivalent of these statements would look like this:

public function multiplyByHundred(value:int):int {
  value = (value * 100);
  return value;
}

To define jumps between opcodes, for use in if statements for example, use the defineJump method.

Let's add an extra boolean argument to the method which will determine if the given integer value will be multiplied by a hundred or a thousand:

methodBuilder.defineArgument("Boolean");

And then change the method body generation like this:

var iffalse:Op = new Op(Opcode.iffalse,[0]);
var jump:Op = new Op(Opcode.jump,[0]);

methodBuilder.addOpcode(Opcode.getlocal_0)
             .addOpcode(Opcode.pushscope)
             .addOpcode(Opcode.getlocal_2)
             .addOp(iffalse)
             .addOpcode(Opcode.getlocal_1)
             .addOpcode(Opcode.pushbyte,[100])
             .addOpcode(Opcode.multiply)
             .addOpcode(Opcode.convert_i)
             .addOpcode(Opcode.setlocal_1)
             .addOp(jump)
             .defineJump(iffalse, new Op(Opcode.getlocal_1))
             .addOpcode(Opcode.pushshort,[1000])
             .addOpcode(Opcode.multiply)
             .addOpcode(Opcode.convert_i)
             .addOpcode(Opcode.setlocal_1)
             .defineJump(jump, new Op(Opcode.getlocal_1))
             .addOpcode(Opcode.returnvalue);

The actionscript source equivalent of these statements would look like this:

public function multiplyByHundred(value:int, reallyDoIt:Boolean):int {
  if (reallyDoIt) {
    value = value * 100;
  } else {
    value = value * 1000;
  }
  return value;
}

Defining method bodies using a string source

A second way of adding the method body is by defining the assembly source fully as a string:

var source:String = (<![CDATA[
		     getlocal_0
		     pushscope
		     getlocal_2
		     iffalse L0
		     getlocal_1
		     pushbyte 100
		     multiply
		     convert_i
		     setlocal_1
		     jump L1
		     L0:
		     getlocal_1
		     pushshort 1000
		     multiply
		     convert_i
		     setlocal_1
		     L1:
		     getlocal_1
		     returnvalue
	]]>).toString();

methodBuilder.addAsmSource(source);

The format of the source is quite similar to the results found in a swfdump output.

The source needs to adhere to a few simple rules:

  • Each instruction and its operands need to be on a separate line.
  • Separate instructions and operands with spaces and/or tabs.
  • Labels are referenced by name. I.e. iffalse L1
  • Labels are defined by a name suffixed by a colon. I.e. L1:

Any exceptions referenced in the opcode collection can be added using the defineExceptionInfo() method:

var exceptionInfoBuilder:IExceptionInfoBuilder = methodBuilder.defineExceptionInfo();

Other properties that can be set on the IMethodBuilder include:

  • isOverride
  • isFinal
  • isStatic

Generating a constructor

By default a parameterless constructor is generated for the class, if a different constructor is needed invoke the defineConstructor() method and use the resulting ICtorBuilder instance to define it:

var ctorBuilder:ICtorBuilder = classBuilder.defineConstructor();

The ICtorBuilder instance works the same as an IMethodBuilder instance with the exception that any returnType or visibility assignments will be ignored.

Multiple calls to defineConstructor() will yield the same ICtorBuilder instance.

Generating metadata

Methods, properties, accessors and classes themselves can be annotated with metadata using an IMetadataBuilder instance:

var metadataBuilder:IMetadataBuilder = methodBuilder.defineMetadata("Inject");

To add arguments to the metadata definition invoke the defineArgument() method:

var metadataArgument:MetadataArgument = metadataBuilder.defineArgument();
metadataArgument.key = "name";
metadataArgument.value = "objectName";

The actionscript source equivalent of this statement would look like this:

[Inject(name="objectName")]

Generating interfaces

Generating interfaces is mostly the same as generating classes, with the exception that constructors and properties cannot be defined and any method or accessor that is added will automatically receive public visibility.

var interfaceBuilder:IInterfaceBuilder = packageBuilder.defineInterface("RuntimeInterface");

If the interface needs to extend other interfaces pass a list of their fully qualified names to the constructor like this:

var interfaceBuilder:IInterfaceBuilder = packageBuilder.defineInterface("RuntimeInterface",["flash.events.IEventDispatcher"]);

The actionscript source equivalent of this statement would look like this:

package com.classes.generated {
  public interface RuntimeInterface extends IEventDispatcher {
  }
}

Generating namespaces

To generate a new namespace use the defineNamespace() method on the IPackageBuilder.

abcBuilder.addPackage("com.myclasses.namespaces").addNamespace('my_custom_namespace','http://www.mynamespaces.com/custom');

The actionscript source equivalent of this statement would look like this:

package com.myclasses.namespaces {
  public namespace my_custom_namespace = "http://www.mynamespaces.com/custom";
}

Loading the generated classes into the AVM

Finally, after defining the necessary classes and interfaces they need to be loaded into the AVM so they can be instantiated.
There are two ways of doing this, the shortest way is using the buildAndLoad() method on the IAbcBuilder instance.

The process of loading class definitions is asynchronous so first these event listeners need to be added:

abcBuilder.addEventListener(Event.COMPLETE, loadedHandler);
abcBuilder.addEventListener(IOErrorEvent.IO_ERROR, errorHandler);
abcBuilder.addEventListener(IOErrorEvent.VERIFY_ERROR, errorHandler);

After that invoke the buildAndLoad() method:

abcBuilder.buildAndLoad();

In the loadedHandler method a reference to a generated class can be retrieved and instantiated like this:

function loadedHandler(event:Event):void {
  var clazz:Class = ApplicationDomain.currentDomain.getDefinition("com.classes.generated.RuntimeClass") as Class;
  var instance:Object = new clazz();
  var i:int = instance.multiplyByHundred(10);
  // i == 1000
}

The buildAndLoad() method has two optional arguments:

public function buildAndLoad(applicationDomain:ApplicationDomain = null, newApplicationDomain:ApplicationDomain = null):AbcFile;

The first ApplicationDomain argument will be the domain that contains any class definitions that are used as superclasses in the generated classes. This domain will be used to retrieve reflection information about the superclasses needed by the class generation process. If no ApplicationDomain is passed then ApplicationDomain.currentDomain will be used.

The second ApplicationDomain argument will be used to load the generated classes into. Again, when no ApplicationDomain is passed then ApplicationDomain.currentDomain will be used.

The second way to load the generated classes is like this:

var abcFile:AbcFile = abcBuilder.build();
var abcLoader:AbcClassLoader = new AbcClassLoader();

abcLoader.addEventListener(Event.COMPLETE, loadedHandler);
abcLoader.addEventListener(IOErrorEvent.IO_ERROR, errorHandler);
abcLoader.addEventListener(IOErrorEvent.VERIFY_ERROR, errorHandler);

abcLoader.loadAbcFile(abcFile);

Exporting the generated classes to file

If there is a need to persist the generated classes to disk, follow these steps:

var binarySwf:ByteArray = abcBuilder.buildAndExport();
var file:FileReference = new FileReference();
file.save(binarySwf, "MyGeneratedClasses.swf");