Closed nickwinder closed 11 months ago
Hi there,
While DNNE is not setup for this, there is a way to do .NET on Android using Mono.
What needs to happen is a few things.
${dotnetInstallDir}\dotnet\packs\Microsoft.NETCore.App.Runtime.Mono.android-arm64
myAndroidProject\src\main\jniLibs\arm64-v8a
private fun unpackAssetsDlls()
{
val assetManager = context.assets
val files = assetManager.list(CommandSystemBuilder.Companion.COMMAND_SYSTEM_ASSETS_DIR) ?: return
val path = Paths.get(_commandSystemDllDir)
Files.createDirectories(path)
for (fileName in files) {
if (fileName.endsWith(".dll")) {
copyAssetFile(fileName, fileName)
}
}
// Get the private corelib for the right architecture.
// Check for 64 bit first and keep track if we found one. If
// we don't find one, it is unsupported.
val supportedABIs = Build.SUPPORTED_ABIS;
var selectedABI:String? = null;
for(abi in supportedABIs) {
if(abi == "arm64-v8a" || abi == "x86_64" || abi == "armeabi-v7a" || abi == "x86") {
selectedABI = abi
break;
}
}
if(selectedABI == null){
throw IllegalArgumentException("Unsupported architecture")
}
copyAssetFile("${selectedABI}/System.Private.CoreLib.dll", "System.Private.CoreLib.dll")
}
This is what I have in my codebase:
#include <mono/jit/jit.h>
#include <mono/metadata/appdomain.h>
#include <mono/metadata/assembly.h>
#include <mono/metadata/class.h>
#include <mono/metadata/object.h>
#define LOG_INFO(fmt, ...) __android_log_print(ANDROID_LOG_DEBUG, "DOTNET", fmt, ##__VA_ARGS__)
#define LOG_ERROR(fmt, ...) __android_log_print(ANDROID_LOG_ERROR, "DOTNET", fmt, ##__VA_ARGS__)
static MonoAssembly* mono_droid_load_assembly (const char *name, const char *culture)
{
char filename [1024];
char path [1024];
int res;
const char * bundle_path = BundlePath::getInstance()->getPath();
LOG_INFO ("assembly_preload_hook: %s %s %s\n", name, culture, bundle_path);
int len = strlen (name);
int has_extension = len > 3 && name [len - 4] == '.' && (!strcmp ("exe", name + (len - 3)) || !strcmp ("dll", name + (len - 3)));
// add extensions if required.
strlcpy (filename, name, sizeof (filename));
if (!has_extension) {
strlcat (filename, ".dll", sizeof (filename));
}
if (culture && strcmp (culture, ""))
res = snprintf (path, sizeof (path) - 1, "%s/%s/%s", bundle_path, culture, filename);
else
res = snprintf (path, sizeof (path) - 1, "%s/%s", bundle_path, filename);
assert (res > 0);
struct stat buffer;
if (stat (path, &buffer) == 0) {
MonoAssembly *assembly = mono_assembly_open (path, NULL);
assert (assembly);
return assembly;
}
return NULL;
}
static MonoAssembly* mono_droid_assembly_preload_hook (MonoAssemblyName *aname, char **assemblies_path, void* user_data)
{
const char *name = mono_assembly_name_get_name (aname);
const char *culture = mono_assembly_name_get_culture (aname);
return mono_droid_load_assembly (name, culture);
}
// call this in JNI onLoad
mono_install_assembly_preload_hook (mono_droid_assembly_preload_hook, NULL);
mono_jit_init("MyCompany::MyDomainName");
In JNI you will need to call your .NET functions that are marked with "UnmanagedCallersOnly" by invoking the Mono apis. Here is an example:
uint32_t MonoCommandSystem::InvokeMonoDOTNETMethod(const char* methodName, uint paramCount, void** params)
{
// get NativeDOTNETClass and method
_MonoClass * staticClass = mono_class_from_name(m_image, "myDotnetNativeAssembly", "NativeDOTNETClass");
_MonoMethod * staticMethod = mono_class_get_method_from_name( staticClass, methodName, paramCount);
// Call static method
auto result = mono_runtime_invoke(staticMethod, NULL, params, NULL);
// unbox result and check error
void* resultPtr = mono_object_unbox(result);
uint32_t * resultInt = reinterpret_cast<uint32_t*>(resultPtr);
return *resultInt;
}
Depending on your application, you might need to have a map of mono objects to JNI/JVM objects to make sure the JVM garbage collector and the Mono garbage collector don't destroy things before you want them to.
In addition if you have any callbacks, you will need to be very careful about managing the mono thread you are attached to so you don't try to access something that you don't have permission to.
This is definitely an advanced way of doing things and has many pitfalls if you aren't familiar with JNI, JVM, garbage collectors, and Mono. I would only go down this path if you have no other choice. Though things are being made easier in .NET 8, they are far from doing this all for you. I hope this helps :)
@chschrae This is a great answer, thanks! I'll see what I can spin up.
small edit that my bundle_path variable is passed down to the JNI from the app to tell it which directory I put the dlls in
I'm closing this as @chschrae idea seems the most viable.
I've been recently exploring the possibility of supporting Android/iOS platforms calling into .NET code, and up to now I have found very little options.
After finding DNNE, I was able to create a working solution by building the dotnet runtime + libhostfxr + libhostpolicy to create the dependencies needed to build DNNE from scratch for the two targets (actually more because there's multiple archs).
This works, but seems like a convoluted solution which all stems from the fact that iOS and Android .NET solutions utilize Mono, rather than hostfxr which DNNE resolution is based upon.
I'm reaching out to see if I'm missing something fundamental that could simplify the convoluted compilation steps, or possibly modify DNNE to work out the box with Mono instead. Thanks