Luna Tech Series: How We Automagically Remove the APIs You Don’t Need from Your Playable

Anton Shpak
Luna Labs
Published in
7 min readJul 31, 2020

Earlier this year, we added a new feature, Automatic Stubbing, to Luna Playable. Affectionately referred to by Lunarians as Stubber, it reduces the number of changes Luna Partners need to make to their games’ codebases to create performant, lightweight HTML5 playables.

In this instalment of the Luna Tech Series, we share everything you need to know about Stubber.

What is Stubber and why do we need it?

Luna Playable is a tool that allows developers to build lightweight HTML5 playables from existing projects in Unity. To make this possible, we built a powerful HTML5 game engine that provides web-based analogues of Unity’s features and shares a common public interface…

Unity’s engine contains a number of APIs that are unnecessary or unsupported on web, and it’s possible that your existing game may implement some of these. So, when you try to build an HTML5 playable from your existing Unity project with Luna Playable, you may need to remove references to some of those APIs. These generally include haptics, ads SDKs, analytics and engine features not optimised for web.

In the past, a Unity developer would need to manually edit their scripts to remove references to these APIs before they start using Luna Playable. With Stubber, it eliminates the need for those changes by auto-generating stubs for any class the developer wishes the Luna Engine to ignore when compiling their playable.

How it works

To use your code, Luna Playable converts C# sources to plain JS. When you have compiled plugins, Luna is unable to convert those to JS, which means if those APIs are referenced in your scripts the build process will fail.

Stubber takes a namespace or path and locates all assemblies belonging to that namespace or at the specified path. It will then fetch all associated public or internal classes, enumerations, structs and interfaces. With the collected information, the plugin generates all types and methods that can be used in user scripts and implements them as stubs with empty bodies. Of course, there are cases in which type must have a definition in its body. In this case, Stubber will generate a default value.

Automatic Stubbing is especially useful for disabling third-party SDKs that provide non-gameplay functionality like ads, analytics and error reporting without having to edit your code or change your project.

This feature is used if types are contained in the library because if we have sources, Luna Playable converts them by itself.

To take all types from classes to fields, we used the Reflection API, which contains many useful functions for our purposes.

As our main goal is to make code compilable without adding APIs from libraries or with missing APIs of UnityEngine.dll, we can filter all types by public and internal access modifiers. You can’t use an API with other modifiers and we don’t want to waste build size with stubs which will not be used — we have to be very considerate of the final build size as ads platforms typically have tight restrictions for playable package sizes. To filter all public/protected types we are using flags as parameters in the get-type method:

var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static |
BindingFlags.DeclaredOnly | BindingFlags.NonPublic;

// Gather fields that should be generated
var accessibleFields = classType.GetFields( flags ).ToList();

At first, Stubber generates the main type (most often it is a class). Then, it generates all other types in this main type:

Now, let’s think about everything you can reference in your scripts that we need to stub:

Fields

Field — one of the simplest members for stubbing. We only need to check if it is constant by using the method FieldInfo.IsLiteral. In this case, it must contain a value even if it is an empty string for string type.

Let’s not forget about Infinity values as well.

if (  float.IsPositiveInfinity( ( float ) fieldInfo.GetValue( value ) ) ) {
value = " = " + "float.PositiveInfinity";
} else if ( float.IsNegativeInfinity( ( float ) fieldInfo.GetValue( value ) ) ) {
value = " = " + "float.NegativeInfinity";
}

Field can be Action’s part. It can be detected by checking the name — it contains “Action”. Of course, in this case, we don’t generate this field.

Properties

Properties can have an indexed property with the keyword this. You can do this by checking the length of index parameters with the method: PropertyInfo.GetIndexParameters().Length.

It’s important to check whether the property has set and get methods, and generate a field body based on this information. For this purpose, we used methods PropertyInfo.GetSetMethod and PropertyInfo.GetGetMethod.

Delegates

There is no special case for stubbing delegates. But for fetching delegates, we use the following method:

classType.GetNestedTypes( flags ).Where( nestedType => nestedType.BaseType == typeof( MulticastDelegate ) ).ToList()
.ForEach( scriptBuilder.WriteDelegate );

Events

Again, the stubber handles events with ease. There’s not a lot of complexity here!

Constructors

For constructors, we need to check if the main type has a base type, which has its own constructors with parameters. If it does, then we need to generate a default value using the constructor from the base type.

// Checks is base type has constructors with parameters to generate inherited constructors with default valuebaseHasConstructorWithParameters = classType.BaseType .GetConstructors().Any( baseTypeConstructor => baseTypeConstructor.GetParameters().Length > 0 );

If the main type is struct, it’s constructors must initialize all instance fields of the type. So, in this case, every constructor in struct in its body has all fields with a default value.

Methods

The most interesting of all types was the method. It has the most options. If the method is an operator, it needs to convert methodInfo.Name to a symbol. There are many options for symbols. It may also be an explicit or implicit operator and we need to take that into consideration.

public readonly Dictionary<string, string> operatorsAliases = new Dictionary<string, string> {
{ "op_UnaryPlus", "+" },
{ "op_UnaryNegation", "-" },
{ "op_Increment", "++" },
{ "op_Decrement", "--" },
{ "op_LogicalNot", "!" },
{ "op_Addition", "+" },
{ "op_Subtraction", "-" },
{ "op_Multiply", "*" },
{ "op_Division", "/" },
{ "op_BitwiseAnd", "&" },
{ "op_BitwiseOr", "|" },
{ "op_ExclusiveOr", "^" },
{ "op_OnesComplement", "`" },
{ "op_Equality", "==" },
{ "op_Inequality", "!=" },
{ "op_LessThan", "<" },
{ "op_GreaterThan", ">" },
{ "op_LessThanOrEqual", "<=" },
{ "op_GreaterThanOrEqual", ">=" },
{ "op_LeftShift", "<<" },
{ "op_RightShift", ">>" },
{ "op_Modulus", "%" },
{ "op_True", "true" },
{ "op_False", "false" },
{ "op_Implicit", "implicit " },
{ "op_Explicit", "explicit " }
};

A method can use not only out or ref parameters but also the rarer [In, Out] parameters.

There are some cases in which methods are not generated. Method can be part of an event — method name contains prefix “add_” or “remove_” and event name.

Methods also can be related to get/set properties. There is a method we can use for that purpose — MethodBase.IsSpecialName. They are not generated.

If the method is destructor, it too will not be generated. This can be determined by checking the method’s name.

// if method is destructor
if ( method.Name == "Finalize" ) {
return false;
}

A method may also be one of the system methods which is not needed. Here is a list of these method names:

private readonly List<string> methodParametersException = new List<string> {
"Memory",
"ReadOnlyMemory",
"Span",
"ReadOnlySpan",
"ValueTask"
};

Interface fields

Yes, it’s not a type but it’s necessary in case the main type inherits from the interface because we need to generate types from it. We also add a duplicate check.

All those types are gathered and saved as a script. After the whole script is created, it is then added to the Luna Playable build.

What features have been identified

For adding all necessary “using” declarations to user scripts, the Stubber just gathers namespaces of all generated types, formats this list by method list.Distinct and adds it to the final script.

It’s important to check if the main type inherits from the interface because it is imperative to create types from the inherited interface in base type.

There are some cases in which Stubber doesn’t generate type, which we covered earlier. And one more case — if type or its parameters is unsafe, it can be checked by using the method Type.isPointer.

// Don't generate unsafe property
if ( propertyInfo.PropertyType.IsPointer ) {
generateProperty = false;
}

It can be very tricky to use the name of type with the symbol “@”. In this case, if we try to get a name of type, we get a name without this symbol and as a result, it gets an error in the generated script.

This problem can be solved by this method:

/// <summary>
/// Checks is type has correct name. If not - add to name symbol @.
/// </summary>
public string ValidateName( string name ) {
return !codeProvider.IsValidIdentifier( name ) ? "@" + name : name;
}

What’s next

In the future, we want to improve the Stubber to further reduce the final build size of your playable — one way we plan to do this is by only generating code stubs for types used in a project’s user scripts. Expect improvements like this and more to come over the next few months.

We hope Stubber will be a don’t-leave-home-without-it part of your workflow when using Luna Playable. So, be sure to check out our documentation page to learn more about how and when to use it.

As always, we’re eager to hear your feedback so please reach out and tell us what you think! In the meanwhile, you can follow us here on Medium, Linkedin, Twitter, Facebook, or Instagram 🤓

--

--