Unity with Native Android Calls

Unity with Native Android Calls

As I work on a toy project in unity I have came across the challenge of integrating it with android. My project is targetting android, however it doesn’t really need to use any android features so my goal was to bypass JNI and go straight to the native system as much as possible.

My inital goal is just to simple get a scene loaded onto an android emulator but so far I’ve had no luck. The apk seems to be loading, the application icon shows on the android emulator but starting up the unity app just crashes the emulator, more than this the logs don’t really give much to go on.

A few logs of note from my app are

2024-08-02 20:37:39.356 10480-10480 IL2CPP                  com.io.emu.Emu                       I  JNI_OnLoad
...
2024-08-02 20:37:48.296 10480-10529 Unity                   com.io.emu.Emu                       I  Company Name: io.emu
2024-08-02 20:37:48.296 10480-10529 Unity                   com.io.emu.Emu                       I  Product Name: Emu
2024-08-02 20:37:48.424 10480-10529 platform                com.io.emu.Emu                       E  Failed to open rendernode: No such file or directory
...
2024-08-02 20:38:29.917 10480-10619 goldfish_vulkan         com.io.emu.Emu                       D  ensureSyncDeviceFd: created sync device for current Vulkan process: 135

I include the last log simply because it is the last log, doesn’t mean much to me, so none of these logs really mean much to me except that my app is being recognised in some way. The error mentions having trouble finding some directory or file (no idea what one). An important point is that none of my logs are actually showing, that is when I manually log something in my code, so all this seems to be related to unity setup.

The logs outside my namespace end with these

2024-08-02 20:38:29.837  9307-10013 WorkSourceUtil          com.google.android.gms               E  Could not find package: com.google.android.gms.westworld
2024-08-02 20:38:29.857   532-944   ProcessStats            system_server                        W  Tracking association SourceState{1a4117d com.google.android.gms.persistent/10130 BFgs #17943} whose proc state 4 is better than process ProcessState{ba155bd com.google.android.gms/10130 pkg=com.google.android.gms (sub)} proc state 5 (1156 skipped)
2024-08-02 20:38:29.917 10480-10619 goldfish_vulkan         com.io.emu.Emu                       D  ensureSyncDeviceFd: created sync device for current Vulkan process: 135

Again nothing that means a lot to me and nothing I would expect to crash the app. I am using NLog with a custom logging config, the config itself is being loaded into the assets folder in the apk

        // On Android, load config from the StreamingAssets folder
        configPath = Path.Combine(Application.streamingAssetsPath, "NLog.config");
        Debug.Log($"LoggingManager: Loading NLog config from StreamingAssets at {configPath}.");

        using (UnityWebRequest www = UnityWebRequest.Get(configPath))
        {
          yield return www.SendWebRequest();

          if (www.result != UnityWebRequest.Result.Success)
          {
            Debug.LogError($"LoggingManager: Failed to load NLog configuration from {configPath}: {www.error}");
            throw new System.Exception($"Failed to load NLog configuration from {configPath}: {www.error}");
          }

          Debug.Log("LoggingManager: NLog config loaded successfully from StreamingAssets.");
          LoadNLogConfigFromText(www.downloadHandler.text);
        }

Even if there is some issue loading NLog config I am also logging using unity’s own logging method which is also not showing in logcat.

My plan now is to try and load a much simpler unity apk onto the emulator. This project is large and complex with native code, VR and a lot of scenes, third-party libraries etc so it might be challenging to debug. it also takes a long time to build the apk. I am loading this as a streaming asset.

I created a basic unity project with a single cube and camera, set it up to build android. I ran a pixel 3 emulator but it kept crashing with something gms related, so instead I tried running it in headless mode with this

emulator -avd Pixel_3a_API_34 -no-window -gpu off

I needed to use gpu off to disable hardware acceleration, maybe because I am using a laptop with an incompatible gpu or something, to save time rebuilding from unity each time I built it once. I loaded my apk then checked the package from the list with these commands

adb install "Build\build.apk"
adb shell pm list packages

I could then find my package from the list and run it like this

adb shell monkey -p com.ryan.AndroidDebug -c android.intent.category.LAUNCHER 1

This seems to be an improvement as it didn’t crash the emulator like previous times and can be seen loading in the emulator logs

I0803 22:38:28.125173   11932 VkDecoderGlobalState.cpp:447] Creating Vulkan instance for engine: Unity

And I can see something going on in logcat

2024-08-03 22:38:25.432  7166-7202  Unity                   com.ryan.AndroidDebug                I  SystemInfo CPU = x86-64 SSE3 SSE4.1 SSE4.2 AVX, Cores = 4, Memory = 1973mb
2024-08-03 22:38:25.432  7166-7202  Unity                   com.ryan.AndroidDebug                I  ApplicationInfo 'com.ryan.AndroidDebug', Version '0.1', Min API Level '32', Target API Level '32'
2024-08-03 22:38:25.432  7166-7202  Unity                   com.ryan.AndroidDebug                I  Built from '2023.2/staging' branch, Version '2023.2.3f1 (21747dafc6ee)', Build type 'Development', Scripting Backend 'il2cpp', CPU 'x86_64', Stripping 'Enabled'
2024-08-03 22:38:25.433  7166-7202  Unity                   com.ryan.AndroidDebug                I  Device Model 'Google sdk_gphone64_x86_64', OS 'Android OS 14 (API 34)'
...
2024-08-03 22:38:25.719  7166-7202  Unity                   com.ryan.AndroidDebug                I  Company Name: ryan
2024-08-03 22:38:25.719  7166-7202  Unity                   com.ryan.AndroidDebug                I  Product Name: Android Debug
...
2024-08-03 22:38:25.814  7166-7202  platform                com.ryan.AndroidDebug                E  Failed to open rendernode: No such file or directory
...
2024-08-03 22:38:25.995  7166-7202  EGL_emulation           com.ryan.AndroidDebug                E  eglCreateContext: EGL_BAD_CONFIG: no ES 3.1 support
2024-08-03 22:38:25.995  7166-7202  EGL_emulation           com.ryan.AndroidDebug                E  tid 7202: eglCreateContext(1828): error 0x3005 (EGL_BAD_CONFIG)
2024-08-03 22:38:25.995  7166-7202  Unity                   com.ryan.AndroidDebug                D  [EGL] ES3.1 not supported
2024-08-03 22:38:25.995  7166-7202  Unity                   com.ryan.AndroidDebug                D  [EGL] Request: ES 3.1 RGB 000 0/0
2024-08-03 22:38:25.995  7166-7202  Unity                   com.ryan.AndroidDebug                D  [EGL] Request: ES 3.0 RGB 000 0/0
2024-08-03 22:38:25.996  7166-7202  EGL_emulation           com.ryan.AndroidDebug                E  [getAttribValue] Bad attribute idx
2024-08-03 22:38:25.996  7166-7202  EGL_emulation           com.ryan.AndroidDebug                E  tid 7202: eglGetConfigAttrib(1277): error 0x3004 (EGL_BAD_ATTRIBUTE)
...
2024-08-03 22:38:27.547   531-557   ActivityTaskManager     system_server                        I  Fully drawn com.ryan.AndroidDebug/com.unity3d.player.UnityPlayerActivity: +3s683ms

Some familiar error logs are there like in my original app so maybe it was a false alarm. The last log seems promising as it has drawn the unity activity ‘UnityPlayerActivity’ which is the entrypoint to the app.

I then stopped the app

adb shell am force-stop com.ryan.AndroidDebug

The logs didn’t look pretty but it is confirmation it was running I guess and just didn’t stop gracefully since I forced it

2024-08-03 22:38:27.547   531-557   ActivityTaskManager     system_server                        I  Fully drawn com.ryan.AndroidDebug/com.unity3d.player.UnityPlayerActivity: +3s683ms
2024-08-03 23:02:38.020   531-944   ActivityTaskManager     system_server                        W  Force removing ActivityRecord{34c3e14 u0 com.ryan.AndroidDebug/com.unity3d.player.UnityPlayerActivity t8 f}}: app died, no saved state
2024-08-03 23:02:38.029   881-924   WindowManagerShell      com.android.systemui                 V  Transition requested: android.os.BinderProxy@e4708fa TransitionRequestInfo { type = CLOSE, triggerTask = TaskInfo{userId=0 taskId=8 displayId=0 isRunning=false baseIntent=Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.ryan.AndroidDebug/com.unity3d.player.UnityPlayerActivity } baseActivity=null topActivity=null origActivity=null realActivity=ComponentInfo{com.ryan.AndroidDebug/com.unity3d.player.UnityPlayerActivity} numActivities=0 lastActiveTime=462248 supportsMultiWindow=false resizeMode=0 isResizeable=false minWidth=-1 minHeight=-1 defaultMinSize=220 token=WCT{android.window.IWindowContainerToken$Stub$Proxy@4572eab} topActivityType=1 pictureInPictureParams=null shouldDockBigOverlays=false launchIntoPipHostTaskId=-1 lastParentTaskIdBeforePip=-1 displayCutoutSafeInsets=null topActivityInfo=null launchCookies=[] positionInParent=Point(0, 0) parentTaskId=-1 isFocused=true isVisible=true isVisibleRequested=true isSleeping=false topActivityInSizeCompat=false topActivityEligibleForLetterboxEducation= false topActivityLetterboxed= false isFromDoubleTap= false topActivityLetterboxVerticalPosition= -1 topActivityLetterboxHorizontalPosition= -1 topActivityLetterboxWidth=-1 topActivityLetterboxHeight=-1 locusId=null displayAreaFeatureId=1 cameraCompatControlState=hidden}, remoteTransition = null, displayChange = null }
2024-08-03 23:02:38.051   531-2457  WindowManager           system_server                        I  WIN DEATH: Window{d42eaed u0 com.ryan.AndroidDebug/com.unity3d.player.UnityPlayerActivity}
2024-08-03 23:02:38.052   531-2457  InputManager-JNI        system_server                        W  Input channel object 'd42eaed com.ryan.AndroidDebug/com.unity3d.player.UnityPlayerActivity (client)' was disposed without first being removed with the input manager!
2024-08-03 23:02:38.549   531-557   WindowManager           system_server                        V  Sent Transition #9 createdAt=08-03 23:02:38.020 via request=TransitionRequestInfo { type = CLOSE, triggerTask = TaskInfo{userId=0 taskId=8 displayId=0 isRunning=false baseIntent=Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.ryan.AndroidDebug/com.unity3d.player.UnityPlayerActivity } baseActivity=null topActivity=null origActivity=null realActivity=ComponentInfo{com.ryan.AndroidDebug/com.unity3d.player.UnityPlayerActivity} numActivities=0 lastActiveTime=462248 supportsMultiWindow=false resizeMode=0 isResizeable=false minWidth=-1 minHeight=-1 defaultMinSize=220 token=WCT{RemoteToken{5f7f0e5 Task{9d8b6bd #8 type=standard A=10190:com.ryan.AndroidDebug}}} topActivityType=1 pictureInPictureParams=null shouldDockBigOverlays=false launchIntoPipHostTaskId=-1 lastParentTaskIdBeforePip=-1 displayCutoutSafeInsets=null topActivityInfo=null launchCookies=[] positionInParent=Point(0, 0) parentTaskId=-1 isFocused=true isVisible=true isVisibleRequested=true isSleeping=false topActivityInSizeCompat=false topActivityEligibleForLetterboxEducation= false topActivityLetterboxed= false isFromDoubleTap= false topActivityLetterboxVerticalPosition= -1 topActivityLetterboxHorizontalPosition= -1 topActivityLetterboxWidth=-1 topActivityLetterboxHeight=-1 locusId=null displayAreaFeatureId=1 cameraCompatControlState=hidden}, remoteTransition = null, displayChange = null }
2024-08-03 23:02:38.849   531-945   WindowManager           system_server                        W  Exception thrown during dispatchAppVisibility Window{d42eaed u0 com.ryan.AndroidDebug/com.unity3d.player.UnityPlayerActivity EXITING}
                                                                                                    android.os.DeadObjectException

Adding a simple log statement with unity has indeed logged in logcat after attaching to the cube

2024-08-04 14:43:54.343  5115-5152  Unity                   com.ryan.AndroidDebug                I  I CUBE HAVE INITIALIZED

I recreated this scene in the original project – it didn’t work to begin with, I then removed all scenes from the build even though they were not checked and so shouldn’t be included. It then worked, so then I tried adding my custom components to this scene and everything started logging.

In summary I don’t think my scene was even starting, my understanding was the first scene in the list would get initialized with the app and only checked scenes would be included.

I discovered I can use the unity “patch and run” feature instead of “build and run” for android, this is much faster. I tested this by swapping out a dll to fix some native code issue and it did pick up the changes.

The challenge I am facing now is to get some logging working for android, I am using a logging delegate in x86_64 windows architectures which seems to work. In android I am doing this in native code

#define LOG_TAG "PdfiumLib"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
...
typedef void (*LogCallbackDelegate)(const char*);
static LogCallbackDelegate g_logCallback = NULL;
...
    void SetLogCallback(LogCallbackDelegate callback)
    {
        g_logCallback = callback;
    }

    void LogMessage(const char* file, const char* function, int line, const char* message) {
        if (g_logCallback) {
            char buffer[1024];
            snprintf(buffer, sizeof(buffer), "[%s:%d %s] %s", file, line, function, message);
            g_logCallback(buffer);
        } else {
            LOGI("[%s:%d %s] %s", file, line, function, message);
        }
    }

The goal is to give me some consistency between my unity logging and the native logging, maybe there are better solutions, but for now this will do if it works.

In the c# code we have

[DllImport(EmuNativePluginDLLName, CallingConvention = DefaultCallingConvention)]
private static extern void SetLogCallback(LogCallbackDelegate logCallback);
...
SetLogCallback(NativeLoggingDelegate);
...
private static void NativeLoggingDelegate(string message)
{
  EmuLogger.Info(message);
}

This essentially creates a logging callback from the native code to my unity app so that I can handle it with the custom NLog config. The current error we are getting is

2024-08-04 20:35:56.748 12300-12352 Unity                   com.io.emu.Emu                       E  NotSupportedException: To marshal a managed method, please add an attribute named 'MonoPInvokeCallback' to the method definition. The method we're attempting to marshal is

This was fixed by adding an annotation which I think tells unity to use MonoPInvoke, I am not sure what this means so I need to read up a bit on managed vs unmanaged functions

[MonoPInvokeCallback(typeof(LogCallbackDelegate))]
private static void NativeLoggingDelegate(string message)
{
  EmuLogger.Info(message);
}

I made a few changes to be able to log at different levels and to improve the logging format, it was initially logging the full file path, so I added this to log only the file name, not ideal but not sure of a better way yet.

#define __FILENAME__ (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)

This gives a more readable log with ALMOST all the info I need

2024-08-09 17:23:37.823  4494-4533  Unity                   com.io.emu.Emu                       I  2024-08-09 17:23:37.8235 INFO (NativeLoggingDelegate:53) [main.cpp:136 GetRenderedImage] Attempting to render image from PDF document.

I say almost because it tells me the log originated in the c# code from “NativeLoggingDelegate:53” which isn’t very useful so I will want it to show the file and line number of the code that is calling the delegate. This should give me the entry point of the c# code and the line of the cpp code.

The second issue is this error

2024-08-09 17:23:31.616  4494-4533  Unity                   com.io.emu.Emu                       E  AndroidJavaException: java.lang.ClassNotFoundException: com.emu.emuandroidplugin.MainActivity
                                                                                                    java.lang.ClassNotFoundException: com.emu.emuandroidplugin.MainActivity
                                                                                                    	at java.lang.Class.classForName(Native Method)
                                                                                                    	at java.lang.Class.forName(Class.java:536)
                                                                                                    	at com.unity3d.player.UnityPlayerForActivityOrService.nativeRender(Native Method)
                                                                                                    	at com.unity3d.player.S.handleMessage(Unknown Source:140)
                                                                                                    	at android.os.Handler.dispatchMessage(Handler.java:102)
                                                                                                    	at android.os.Looper.loopOnce(Looper.java:205)
                                                                                                    	at android.os.Looper.loop(Looper.java:294)
                                                                                                    	at com.unity3d.player.V.run(Unknown Source:24)

I guess my native code is a simple native library and doesn’t really need to use android activities, but android seems to still expect one. After some checking I decided to ignore this error for now and look into it in more detail on a real device rather than a simulator.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *