Wrapping a native SDK for Unity: our challenges and choices

I am Wei Wang from the LINE SDK development team. Last year at LINE Dev Day 2018, we released our new open source LINE SDK for iOS and Android. The SDK provides a modern way to integrate LINE Login and several APIs into your app, helping you create an engaging and personalized user experience.

In addition to app developers, we realized we needed to support mobile game developers as well. Games make up more than 50% of the apps in the App Store and Google Play, and generate the most revenue by far. That’s why we recently released LINE SDK for another important platform, Unity. The LINE SDK for Unity helps game developers use LINE SDK more easily in their next innovative game title.

This article discusses the approach we took in wrapping an existing SDK for Unity, as well as some specific topics for the LINE SDK.

Like our SDK for iOS and Android, the LINE SDK for Unity is open source — find the code on GitHub. Also, read our setup guide to learn how to use the LINE SDK in your Unity games.

Overview

For the Unity SDK, we decided to wrap the existing (iOS and Android) native SDK to provide users with an easy-to-use C# interface. Unlike reimplementing the entire SDK, wrapping the existing functionality brings these advantages:

  • Lower maintenance cost: We can reuse the code of the existing native SDK to reduce maintenance cost. LINE SDK has been open sourced with a high adaptation rate and has been well received. Using the native SDK not only ensures project quality, it also makes new features and fixes in the native SDK be synchronized to the Unity SDK.
  • Leveraging native capabilities: The login feature of the LINE SDK requires a range of system platform features such as logging in with the web view when the LINE app is not installed, and handling data transfers between different apps. Using the native SDK, you can handle these parts in a native way correctly.
  • Familiarity: We provide model and API definitions at the C# level, allowing Unity users to stick to C# conventions while using our SDK, regardless of the differences between iOS and Android platforms.

The basic structure of the LINE SDK for Unity is as below:

Implementation

Let’s go over the implementation of LINE SDK for Unity. 

Remember — the full source code is publicly available on GitHub.

Native plugin for Unity

Unity has extensive support for native plugins. To communicate with the native LINE SDK, we need to build a bridge between the Unity C# world and the “native” world (Swift for iOS, Java for Android).

Handling asynchronous operations

For iOS, we use the DllImport directive to import an exposed interface from Objective-C++ to Unity C#. For Android we use the AndroidJavaObject representation to invoke a method from LINE SDK native:

// iOS interface
[DllImport("__Internal")]
private static extern int foo();
public static int Foo() {
    return foo();
}
 
// Android interface
public static int Foo() {
    var androidObject = new AndroidJavaObject("com.linecorp.linesdk.SomeClass");
    return androidObject.Call<int>("foo");
}

It’s easy to call the API in a synchronous way, returning a value or not. But the LINE SDK also provides a few asynchronous APIs, for login and other network-related operations. For example, the login API has the signature below on iOS:

// Login method signature in LINE SDK Swift.
public func login(
        permissions: Set<LoginPermission> = [.profile],
        options: LoginManagerOptions = [],
        completionHandler completion: @escaping (Result<LoginResult, LineSDKError>) -> Void) -> LoginProcess?

Handling the login result requires a kind of asynchronous callback, as well as passing data between native platforms and Unity game.

The harder way

On iOS, callback across the boundary between C# and C++ is usually done by the Marshal class. This class provides a collection of methods for allocating unmanaged memory, copying unmanaged memory blocks, and converting managed to unmanaged types. For example, we can convert a C# delegate to an unmanaged function pointer, then pass a C# method to the native side. The parameters of the function pointer can be also represented as pointers for unmanaged types.

// Defines a Marshal function pointer to receive callback method from the native side.
delegate void Callback(IntPtr foo);
[DllImport(DllLib)]
private static extern void method([MarshalAs(UnmanagedType.FunctionPtr)] Callback callback);
public static void Method() {
    method(CBMethod);
}
 
[MonoPInvokeCallback(typeof(Callback))]
private static void CBMethod(IntPtr foo) {
    // Convert Int pointer foo to a managed string
    string fooString = Marshal.PtrToStringAuto(foo);
    Debug.Log("Parameter is " + fooString);
}

In the native C++ side (usually written in Objective-C++), we define the extern method as below.

// Defines and invokes callback in the native side.
typedef void (*CallbackT)(const char *foo);
 
extern "C" void method(CallbackT callback);
void method(CallbackT callback) {
    // Do something asynchronously. Then call `callback`.
    //...
    callback("foo sent");
}

By calling the Method() function from the Unity side, the CBMethod() will be invoked with the parameter "foo sent", when the asynchronous operation finishes in the native side.

On Android, we tend to use the observer pattern with a listener interface to get the result from an asynchronous operation. For example, in the LINE SDK for Android, we have the LoginListener interface as below.

// Listener interface for observing login result in the LINE SDK for Android.
public interface LoginListener {
    void onLoginSuccess(@NonNull LineLoginResult result);
    void onLoginFailure(@Nullable LineLoginResult result);
}

In Unity, the AndroidJavaProxy class is perfect to represent any Java interface. We implement a class which inherits from AndroidJavaProxy, then pass the class to a corresponding Android API with a listener parameter as shown below.

// Login proxy in Unity C#, which behaves as a native listener.
class LoginCallback : AndroidJavaProxy
{
    public LoginCallback() : base("com.linecorp.linesdk.$LoginListener") {}
    void onLoginSuccess(AndroidJavaObject result) {
        Debug.Log("Login successfully.");
    }
 
    void onLoginFailure(AndroidJavaObject result) {
        Debug.Log("Login failed.");
    }
}
 
androidObject.Call<int>("login", new LoginCallback());

The Marshal and AndroidJavaProxy classes are powerful and capable of acting as a bridge between the native and the Unity world. However, considering the scale of the LINE SDK and our need to support multiple platforms, they’re not ideal in practice. They have several disadvantages:

  • Introduction of unmanaged memory and untyped object. The Marshal class works with unmanaged pointers and the  AndroidJavaObjectclass or AndroidJavaProxy class works with untyped objects. None of these follow C# conventions.
  • It is hard to extend the SDK. Adding any features may require adding type definition on both the native and Unity side, which doubles the wok required when the SDK evolves.
  • Different approaches in iOS and Android. This makes the mental model complicated.

The easier way

Instead of leveraging the Marshal class and AndroidJavaProxy class, we use a much easier model to handle asynchronous callbacks. That is, a simple UnitySendMessage method from the native side with a token as an identifier and a JSON-serialized parameter to deliver data.

On both iOS and Android, the Unity engine runtime exposes a UnitySendMessage method to send a message to an arbitrary game object (GameObject). If the game object happens to contain a method with the same name as the message, it can respond to that message by invoking that method. This allows us to call a method in Unity from native as shown below.

// Send a message from native to Unity.
UnitySendMessage("GameObjectName", "MethodName", "Message to send");

This method has three parameters:

  • The name of the target GameObject;
  • The script method to call on that object;
  • The message string to pass to the called method.

To respond to this message from the Unity side, a script is required on a game object, named as "GameObjectName"; the script shall contain a method with the name “MethodName", which takes in a single string parameter. 

// This script should be added as a component on "GameObjectName".
class MyClass: MonoBehaviour {
    void MethodName(string parameter) {
        //...
        Debug.Log(parameter);
        // "Message to send"
    }
}

By using an identifier to call native APIs, and passing the same thing back through the UnitySendMessage method, we can identify the caller of an asynchronous operation. On iOS, the basic idea is as follows.

// Passing the `identifier` to track asynchronous operation caller.
 
// Unity - interface for the SDK users.
public static void Method() {
    // Generates an identifier internally.
    var identifier = Guid.NewGuid().ToString();
    Foo(identifier);
}
 
// Unity - wrapper
[DllImport("__Internal")]
private static extern void foo(string identifier);
public static int Foo(string identifier) {
    foo(identifier);
}
 
// iOS
extern "C" void foo(const char *identifier);
void foo(const char *identifier) {
    // Do something asynchronously. Then call `UnitySendMessage` with `identifier`.
    //...
    UnitySendMessage("NativeListener", "CallbackMethod", identifier);
}
 
// In Unity, there is a game object with the name of `NativeListener`.
void CallbackMethod(string identifier) {
    // We know who initializes the operation by checking `identifier`.
    // Do something with the callback from native side.
}

By checking the identifier received, we can determine what to do with a certain callback. This allows communication between Unity and native in an asynchronous way.

Passing data with JSON and Serializable

With the UnitySendMessage method, only a single string value can be sent and received. In the example above, we passed back the identifier value. However, in the LINE SDK, we also need to pass some values in native type to Unity. For instance, the access token, which contains the token value string, expiration date, and token scopes.

We need a way to serialize the data in a format that both native and Unity understand. JSON is a natural choice for exchanging data between languages; it is easy to understand and fully supported by both sides. In the LINE SDK for Unity, we defined a callback payload in the following format.

// Shared structure for token between native side and Unity.
{
  "identifier": "abcdefg...", // The received GUID from Unity.
  "value": {                  // A nested object represents native object.
    "token": "...",
    ...
  }
}

So, instead of just sending back the identifier, we combined it with a value representing the real data we care about and serialize it into a JSON string for sending.

// Serialize a native object to JSON, then send it as a message parameter to Unity.
void foo(const char *identifier) {
    // Do something asynchronously.
    // Then call `UnitySendMessage` with `identifier` and the actual data.
 
    [loginManager loginWithCompletionHandler:^(LineSDKLoginResult * result, NSError *error) {
        NSDictionary *dic = @{
            @"identifier": convertToNSString(identifier),
            @"value": [result toDictionary]
        };
 
        NSData *data = [NSJSONSerialization dataWithJSONObject:dic options:kNilOptions error:nil];
        const char* payload = convertToCString([[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
 
        UnitySendMessage("NativeListener", "CallbackMethod", payload);
    }];
}

In Unity, we use JsonUtility to deserialize the JSON string and convert it back into a certain type as a managed object in C#. This way, we can pass arbitrary data between the native side and the Unity game, as illustrated below.

Integration for LINE SDK on iOS (Swift)

In 2018, we released version 5 of the LINE SDK for iOS, in both Objective-C and Swift. We plan to keep evolving this SDK in the future. Working with Swift has been nice, but since the language has not achieved module stability yet (even in Swift 5), we couldn’t provide the Swift SDK in binary format.

When integrating the native Swift SDK into a Unity project, we need a way to resolve and manage the dependency in the source code. At the same time, Unity exports a project for iOS mainly in Objective-C++, and the native interface must be written in C. Interoperability between C, C++, Objective-C, and Swift is also a challenge. Here are some notes about these challenges.

Installing native SDK

Unity supports exporting a binary library very well; you place the binary under the Assets/Plugins/iOS folder. But since we chose to go with our Swift SDK, we have to build it from source, using the Swift toolchain installed on the developer’s machine. Similar to the native SDK, you can choose CocoaPods or Carthage as the dependency manager. All the integration work is then done by a post process phase.

The PostProcessBuildAttribute attribute provides a hook when Unity exporting finishes. This means we can run a script to set up the dependencies, as well as configure the final project. Our Swift SDK is an open source project, so installing the SDK through CocoaPods or Carthage isn’t too different from adding the LINE SDK to a regular iOS project.

To add the SDK to your iOS project:

  1. Put a Podfile or Cartfile in the exported project root.
  2. Run the command pod install or carthage update on a terminal. Source code of our native SDK will be downloaded from GitHub and compiled when necessary.
  3. Proceed with additional setup, such as linking framework, modifying build settings, and so on.

The interesting part is that we can use Unity’s Xcode APIs to do most of the configuration. All these APIs are under the UnityEditor.iOS.Xcode namespace in Unity, allowing us to manipulate the exported Xcode project with C# code from within the Unity editor.

For example, to change a build setting of Xcode project, you create a PBXProject object and call the SetBuildProperty method on it, as shown below.

// Set a build setting value in exported Xcode project.
 
var path = PBXProject.GetPBXProjectPath(projectRoot);
var project = new PBXProject();
project.ReadFromFile(path);
 
project.SetBuildProperty(target, "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", "YES");

Using the Unity Xcode APIs, we can automate most setups originally needed when integrating a native SDK, including adding the framework to link phase or setting up callback URL schemes.

Interoperability

We have to work with different languages in the LINE SDK for Unity on iOS. The logic of the native SDK is implemented in pure Swift, which contains a few incompatible language features (such as Swift enum and struct) and cannot be used directly in Objective-C.

To use the Swift SDK in Objective-C, we have an additional wrapper — also written in Swift, but using only Objective-C compatible language features. When working with Unity, the exported project has to interact with this wrapper, since it is actually an Objective-C++ project. This means we need to:

  • Enable the clang modules for importing the native Swift SDK to a C++ project;
  • Embed the Swift library in the bundle.
  • Set up the correct search path for runtime.

We can do this with the Unity Xcode API mentioned earlier:

// Enable clang module to use Swift SDK in Objective-C++.
project.SetBuildProperty(target, "CLANG_ENABLE_MODULES", "YES");
 
// Setup Swift environment.
// These two are only necessary when using Carthage. CocoaPods will help us to setup them.
project.SetBuildProperty(target, "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", "YES");
project.SetBuildProperty(target, "LD_RUNPATH_SEARCH_PATHS", "$(inherited) @executable_path/Frameworks");

Using the Xcode API from Unity saved us a lot of time when configuring the project. We recommended that you also use the API to boost project customization.

Listening to system events

When your end user has the LINE app installed on their device, the LINE SDK can open the LINE app as a shortcut for authorization, without requiring a username and password. This means there is communication between your Unity game and LINE app, which often uses a URL scheme to pass the user’s authorization information back to your game. The LINE SDK for Unity needs a way to catch that event.

If you were developing an iOS app, this would be trivial; just add a few lines to your Objective-C or Swift source code and commit it to your code base. However, when working with Unity, your project will be overwritten whenever you export an Xcode project from Unity. Luckily, Unity provides a way to register to the opening app event notification.

// Code exported from Unity. It defines a listener interface for anyone to use.
@protocol AppDelegateListener<LifeCycleListener>
 
//...
 
// notification will be posted from
// - (BOOL)application:(UIApplication*)application openURL:(NSURL*)url sourceApplication:(NSString*)sourceApplication annotation:(id)annotation
// notification user data is the NSDictionary containing all the params
- (void)onOpenURL:(NSNotification*)notification;
 
@end

By creating a class conforming to the AppDelegateListener, then calling the UnityRegisterAppDelegateListener to add it as the notification observer, we can get corresponding future events.

// Register the app delegate listener to receive app life cycle events.
 
@interface LineSDKAppDelegateListener()<AppDelegateListener>
//...
@end
 
@implementation LineSDKAppDelegateListener
- (instancetype)init {
    self = [super init];
    if (self) {
        // ...
        UnityRegisterAppDelegateListener(self);
    }
    return self;
}
 
- (void)onOpenURL:(NSNotification *)notification {
    // Current app is opened (be navigated from LINE app).
}
@end

Find the full code for the LineSDKAppDelegateListener in our GitHub repo.

Integration for LINE SDK on Android

Integration with Android SDK is trivial because we provided a unity-wrapper Android project to wrap native LINE SDK APIs, so that the functions can be easily triggered from the Unity side. The plugin binary is already built and stored under the Assets/Plugins/Android folder. If any modification is required for the unity-wrapper android project, you can always modify it and rebuild the binary again.

Using a custom Gradle template to manage dependency

Unity project for the Android platform uses the Gradle build system. In order to include all necessary dependencies for the unity-wrapper project and LINE SDK library, it’s important to configure a custom Gradle template in the Unity project.

In Unity Player Settings, on the Android settings tab, under Publishing Settings, make sure Custom Gradle Template is enabled. A file named mainTemplate.gradle will be created under the Assets/Plugins/Android folder.

Add the following to the template file:

In the buildscript section

buildscript {
    ext.kotlin_version = '1.3.11'
    ...
 
    dependencies {
        ...
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
 
    }
}

In the dependencies section

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
 
    implementation 'com.linecorp:linesdk:5.0.1'
 
    implementation 'com.google.code.gson:gson:2.8.5'
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
 
    ...
}

For more integration details, refer to LINE SDK for Unity Guide.

Conclusion

We created the LINE SDK for Unity to help you add LINE to your next great game — or to your existing ones. We worked hard to make it simpler to integrate and easier to use. If you’re interested in implementing LINE Login and other LINE APIs in your Unity game, be sure to check out our documentation.

Like the LINE SDK for iOS and LINE SDK for Android, the LINE SDK for Unity is an open source project. If you find any issue or have any suggestion, feel free to open an issue in the repo to discuss.