From 1bb979a7e1db24669d57ff22175792ec8104c82f Mon Sep 17 00:00:00 2001 From: Billy Laws Date: Fri, 19 Jun 2020 21:18:33 +0100 Subject: [PATCH] Introduce new loader JNI for parsing application data and port Kotlin code to use it This will help ease the process of implementing new formats in the future and remove duplicated code. --- app/CMakeLists.txt | 3 +- app/src/main/cpp/{main.cpp => emu_jni.cpp} | 5 +- app/src/main/cpp/loader_jni.cpp | 52 ++++++++++ app/src/main/java/emu/skyline/MainActivity.kt | 34 +++---- .../main/java/emu/skyline/loader/NroLoader.kt | 75 --------------- .../loader/{BaseLoader.kt => RomFile.kt} | 92 +++++++++++++++--- .../skyline/utility/RandomAccessDocument.kt | 94 ------------------- 7 files changed, 154 insertions(+), 201 deletions(-) rename app/src/main/cpp/{main.cpp => emu_jni.cpp} (96%) create mode 100644 app/src/main/cpp/loader_jni.cpp delete mode 100644 app/src/main/java/emu/skyline/loader/NroLoader.kt rename app/src/main/java/emu/skyline/loader/{BaseLoader.kt => RomFile.kt} (59%) delete mode 100644 app/src/main/java/emu/skyline/utility/RandomAccessDocument.kt diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 0e0993ac..e8effd35 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -25,7 +25,8 @@ set(CMAKE_POLICY_DEFAULT_CMP0048 NEW) include_directories(${source_DIR}/skyline) add_library(skyline SHARED - ${source_DIR}/main.cpp + ${source_DIR}/emu_jni.cpp + ${source_DIR}/loader_jni.cpp ${source_DIR}/skyline/common.cpp ${source_DIR}/skyline/nce/guest.S ${source_DIR}/skyline/nce/guest.cpp diff --git a/app/src/main/cpp/main.cpp b/app/src/main/cpp/emu_jni.cpp similarity index 96% rename from app/src/main/cpp/main.cpp rename to app/src/main/cpp/emu_jni.cpp index 83131548..971025d2 100644 --- a/app/src/main/cpp/main.cpp +++ b/app/src/main/cpp/emu_jni.cpp @@ -3,6 +3,7 @@ #include #include +#include "skyline/loader/loader.h" #include "skyline/common.h" #include "skyline/os.h" #include "skyline/jvm.h" @@ -50,7 +51,7 @@ extern "C" JNIEXPORT void Java_emu_skyline_EmulationActivity_executeApplication( auto romUri = env->GetStringUTFChars(romUriJstring, nullptr); logger->Info("Launching ROM {}", romUri); env->ReleaseStringUTFChars(romUriJstring, romUri); - os.Execute(romFd, static_cast(romType)); + os.Execute(romFd, static_cast(romType)); } catch (std::exception &e) { logger->Error(e.what()); } catch (...) { @@ -85,4 +86,4 @@ extern "C" JNIEXPORT jint Java_emu_skyline_EmulationActivity_getFps(JNIEnv *env, extern "C" JNIEXPORT jfloat Java_emu_skyline_EmulationActivity_getFrametime(JNIEnv *env, jobject thiz) { return static_cast(frametime) / 100; -} +} \ No newline at end of file diff --git a/app/src/main/cpp/loader_jni.cpp b/app/src/main/cpp/loader_jni.cpp new file mode 100644 index 00000000..76d55f8a --- /dev/null +++ b/app/src/main/cpp/loader_jni.cpp @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) + +#include "skyline/vfs/os_backing.h" +#include "skyline/loader/nro.h" +#include "skyline/jvm.h" + +extern "C" JNIEXPORT jlong JNICALL Java_emu_skyline_loader_RomFile_initialize(JNIEnv *env, jobject thiz, jint jformat, jint fd) { + skyline::loader::RomFormat format = static_cast(jformat); + + try { + auto backing = std::make_shared(fd); + + switch (format) { + case skyline::loader::RomFormat::NRO: + return reinterpret_cast(new skyline::loader::NroLoader(backing)); + default: + return 0; + } + } catch (const std::exception &e) { + return 0; + } +} + +extern "C" JNIEXPORT jboolean JNICALL Java_emu_skyline_loader_RomFile_hasAssets(JNIEnv *env, jobject thiz, jlong instance) { + return reinterpret_cast(instance)->nacp != nullptr; +} + +extern "C" JNIEXPORT jbyteArray JNICALL Java_emu_skyline_loader_RomFile_getIcon(JNIEnv *env, jobject thiz, jlong instance) { + std::vector buffer = reinterpret_cast(instance)->GetIcon(); + + jbyteArray result = env->NewByteArray(buffer.size()); + env->SetByteArrayRegion(result, 0, buffer.size(), reinterpret_cast(buffer.data())); + + return result; +} + +extern "C" JNIEXPORT jstring JNICALL Java_emu_skyline_loader_RomFile_getApplicationName(JNIEnv *env, jobject thiz, jlong instance) { + std::string applicationName = reinterpret_cast(instance)->nacp->applicationName; + + return env->NewStringUTF(applicationName.c_str()); +} + +extern "C" JNIEXPORT jstring JNICALL Java_emu_skyline_loader_RomFile_getApplicationPublisher(JNIEnv *env, jobject thiz, jlong instance) { + std::string applicationPublisher = reinterpret_cast(instance)->nacp->applicationPublisher; + + return env->NewStringUTF(applicationPublisher.c_str()); +} + +extern "C" JNIEXPORT void JNICALL Java_emu_skyline_loader_RomFile_destroy(JNIEnv *env, jobject thiz, jlong instance) { + delete reinterpret_cast(instance); +} \ No newline at end of file diff --git a/app/src/main/java/emu/skyline/MainActivity.kt b/app/src/main/java/emu/skyline/MainActivity.kt index 935f0dce..d19d19e5 100644 --- a/app/src/main/java/emu/skyline/MainActivity.kt +++ b/app/src/main/java/emu/skyline/MainActivity.kt @@ -28,9 +28,8 @@ import emu.skyline.adapter.AppAdapter import emu.skyline.adapter.AppItem import emu.skyline.adapter.GridLayoutSpan import emu.skyline.adapter.LayoutType -import emu.skyline.loader.BaseLoader -import emu.skyline.loader.NroLoader -import emu.skyline.utility.RandomAccessDocument +import emu.skyline.loader.RomFile +import emu.skyline.loader.RomFormat import kotlinx.android.synthetic.main.main_activity.* import kotlinx.android.synthetic.main.titlebar.* import java.io.File @@ -52,31 +51,34 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick /** * This adds all files in [directory] with [extension] as an entry in [adapter] using [loader] to load metadata */ - private fun addEntries(extension : String, loader : BaseLoader, directory : DocumentFile, found : Boolean = false) : Boolean { + private fun addEntries(extension : String, romFormat : RomFormat, directory : DocumentFile, found : Boolean = false) : Boolean { var foundCurrent = found directory.listFiles().forEach { file -> if (file.isDirectory) { - foundCurrent = addEntries(extension, loader, file, foundCurrent) + foundCurrent = addEntries(extension, romFormat, file, foundCurrent) } else { if (extension.equals(file.name?.substringAfterLast("."), ignoreCase = true)) { - val document = RandomAccessDocument(this, file) + val romFd = contentResolver.openFileDescriptor(file.uri, "r")!! + val romFile = RomFile(this, romFormat, romFd) - if (loader.verifyFile(document)) { - val entry = loader.getAppEntry(document, file.uri) + if (romFile.valid()) { + romFile.use { + val entry = romFile.getAppEntry(file.uri) - runOnUiThread { - if (!foundCurrent) { - adapter.addHeader(loader.format.name) + runOnUiThread { + if (!foundCurrent) { + adapter.addHeader(romFormat.name) + } + + adapter.addItem(AppItem(entry)) } - adapter.addItem(AppItem(entry)) + foundCurrent = true } - - foundCurrent = true } - document.close() + romFd.close(); } } } @@ -109,7 +111,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick try { runOnUiThread { adapter.clear() } - val foundNros = addEntries("nro", NroLoader(this), DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!) + val foundNros = addEntries("nro", RomFormat.NRO, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!) runOnUiThread { if (!foundNros) diff --git a/app/src/main/java/emu/skyline/loader/NroLoader.kt b/app/src/main/java/emu/skyline/loader/NroLoader.kt deleted file mode 100644 index fdcf3853..00000000 --- a/app/src/main/java/emu/skyline/loader/NroLoader.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * SPDX-License-Identifier: MPL-2.0 - * Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) - */ - -package emu.skyline.loader - -import android.content.Context -import android.graphics.BitmapFactory -import android.net.Uri -import emu.skyline.utility.RandomAccessDocument -import java.io.IOException - -/** - * This loader is used to load in NRO (Nintendo Relocatable Object) files (https://switchbrew.org/wiki/NRO) - */ -internal class NroLoader(context : Context) : BaseLoader(context, RomFormat.NRO) { - /** - * This is used to get the [AppEntry] for the specified NRO - */ - override fun getAppEntry(file : RandomAccessDocument, uri : Uri) : AppEntry { - return try { - file.seek(0x18) // Skip to NroHeader.size - - val asetOffset = Integer.reverseBytes(file.readInt()) - file.seek(asetOffset.toLong()) // Skip to the offset specified by NroHeader.size - - val buffer = ByteArray(4) - file.read(buffer) - if (String(buffer) != "ASET") throw IOException() - file.skipBytes(0x4) - - val iconOffset = java.lang.Long.reverseBytes(file.readLong()) - val iconSize = Integer.reverseBytes(file.readInt()) - if (iconOffset == 0L || iconSize == 0) throw IOException() - file.seek(asetOffset + iconOffset) - - val iconData = ByteArray(iconSize) - file.read(iconData) - val icon = BitmapFactory.decodeByteArray(iconData, 0, iconSize) - file.seek(asetOffset + 0x18.toLong()) - - val nacpOffset = java.lang.Long.reverseBytes(file.readLong()) - val nacpSize = java.lang.Long.reverseBytes(file.readLong()) - if (nacpOffset == 0L || nacpSize == 0L) throw IOException() - file.seek(asetOffset + nacpOffset) - - val name = ByteArray(0x200) - file.read(name) - - val author = ByteArray(0x100) - file.read(author) - - AppEntry(String(name).substringBefore((0.toChar())), String(author).substringBefore((0.toChar())), format, uri, icon) - } catch (e : IOException) { - AppEntry(context, format, uri) - } - } - - /** - * This verifies if [file] is a valid NRO file - */ - override fun verifyFile(file : RandomAccessDocument) : Boolean { - try { - file.seek(0x10) // Skip to NroHeader.magic - - val buffer = ByteArray(4) - file.read(buffer) - if (String(buffer) != "NRO0") return false - } catch (e : IOException) { - return false - } - return true - } -} diff --git a/app/src/main/java/emu/skyline/loader/BaseLoader.kt b/app/src/main/java/emu/skyline/loader/RomFile.kt similarity index 59% rename from app/src/main/java/emu/skyline/loader/BaseLoader.kt rename to app/src/main/java/emu/skyline/loader/RomFile.kt index 811ca1fb..d27189b5 100644 --- a/app/src/main/java/emu/skyline/loader/BaseLoader.kt +++ b/app/src/main/java/emu/skyline/loader/RomFile.kt @@ -10,8 +10,9 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri +import android.os.ParcelFileDescriptor import android.provider.OpenableColumns -import emu.skyline.utility.RandomAccessDocument +import android.view.Surface import java.io.IOException import java.io.ObjectInputStream import java.io.ObjectOutputStream @@ -21,10 +22,10 @@ import java.util.* /** * An enumeration of all supported ROM formats */ -enum class RomFormat { - NRO, - XCI, - NSP, +enum class RomFormat(val format: Int){ + NRO(0), + XCI(1), + NSP(2), } /** @@ -69,7 +70,7 @@ class AppEntry : Serializable { */ var uri : Uri - constructor(name : String, author : String, format : RomFormat, uri : Uri, icon : Bitmap) { + constructor(name : String, author : String, format : RomFormat, uri : Uri, icon : Bitmap?) { this.name = name this.author = author this.icon = icon @@ -123,16 +124,81 @@ class AppEntry : Serializable { } /** - * This class is used as the base class for all loaders + * This class is used as interface between libskyline and Kotlin for loaders */ -internal abstract class BaseLoader(val context : Context, val format : RomFormat) { +internal class RomFile(val context : Context, val format : RomFormat, val file : ParcelFileDescriptor) : AutoCloseable { /** - * This is used to get the [AppEntry] for the specified [file] at the supplied [uri] + * This is a pointer to the corresponding C++ Loader class */ - abstract fun getAppEntry(file : RandomAccessDocument, uri : Uri) : AppEntry + var instance : Long + + init { + System.loadLibrary("skyline") + + instance = initialize(format.ordinal, file.fd) + } /** - * This returns if the supplied [file] is a valid ROM or not + * This allocates and initializes a new loader object + * @param format The format of the ROM + * @param romFd A file descriptor of the ROM + * @return A pointer to the newly allocated object, or 0 if the ROM is invalid */ - abstract fun verifyFile(file : RandomAccessDocument) : Boolean -} + private external fun initialize(format : Int, romFd : Int) : Long + + /** + * @return Whether the ROM contains assets, such as an icon or author information + */ + private external fun hasAssets(instance : Long) : Boolean + + /** + * @return A ByteArray containing the application's icon as a bitmap + */ + private external fun getIcon(instance : Long) : ByteArray + + /** + * @return A String containing the name of the application + */ + private external fun getApplicationName(instance : Long) : String + + /** + * @return A String containing the publisher of the application + */ + private external fun getApplicationPublisher(instance : Long) : String + + /** + * This destroys an existing loader object and frees it's resources + */ + private external fun destroy(instance : Long) + + /** + * This is used to get the [AppEntry] for the specified NRO + */ + fun getAppEntry(uri : Uri) : AppEntry { + return if (hasAssets(instance)) { + val rawIcon = getIcon(instance) + val icon = if (rawIcon.size != 0) BitmapFactory.decodeByteArray(rawIcon, 0, rawIcon.size) else null + + AppEntry(getApplicationName(instance), getApplicationPublisher(instance), format, uri, icon) + } else { + AppEntry(context, format, uri) + } + } + + /** + * This checks if the currently loaded ROM is valid + */ + fun valid() : Boolean { + return instance != 0L + } + + /** + * This destroys the C++ loader object + */ + override fun close() { + if (valid()) { + destroy(instance) + instance = 0 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/emu/skyline/utility/RandomAccessDocument.kt b/app/src/main/java/emu/skyline/utility/RandomAccessDocument.kt deleted file mode 100644 index 60b948bb..00000000 --- a/app/src/main/java/emu/skyline/utility/RandomAccessDocument.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * SPDX-License-Identifier: MPL-2.0 - * Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) - */ - -package emu.skyline.utility - -import android.content.Context -import android.os.ParcelFileDescriptor -import androidx.documentfile.provider.DocumentFile -import java.nio.ByteBuffer - -/** - * This is made as a parallel to [java.io.RandomAccessFile] for [DocumentFile]s - * - * @param parcelFileDescriptor The file descriptor for the [DocumentFile] - */ -class RandomAccessDocument(private var parcelFileDescriptor : ParcelFileDescriptor) { - /** - * The actual file descriptor for the [DocumentFile] as an [FileDescriptor] object - */ - private val fileDescriptor = parcelFileDescriptor.fileDescriptor - - /** - * The current position of where the file is being read - */ - private var position : Long = 0 - - /** - * The constructor sets [parcelFileDescriptor] by opening a read-only FD to [file] - */ - constructor(context : Context, file : DocumentFile) : this(context.contentResolver.openFileDescriptor(file.uri, "r")!!) - - /** - * This reads in as many as possible bytes into [array] (Generally [array].size) - * - * @return The amount of bytes read from the file - */ - fun read(array : ByteArray) : Int { - val bytesRead = android.system.Os.pread(fileDescriptor, array, 0, array.size, position) - position += bytesRead - return bytesRead - } - - /** - * This reads in as many as possible bytes into [buffer] (Generally [buffer].array().size) - * - * @return The amount of bytes read from the file - */ - fun read(buffer : ByteBuffer) : Int { - val bytesRead = android.system.Os.pread(fileDescriptor, buffer.array(), 0, buffer.array().size, position) - position += bytesRead - return bytesRead - } - - /** - * This returns a single [Long] from the file at the current [position] - */ - fun readLong() : Long { - val buffer : ByteBuffer = ByteBuffer.allocate(Long.SIZE_BYTES) - read(buffer) - return buffer.long - } - - /** - * This returns a single [Int] from the file at the current [position] - */ - fun readInt() : Int { - val buffer : ByteBuffer = ByteBuffer.allocate(Int.SIZE_BYTES) - read(buffer) - return buffer.int - } - - /** - * This sets [RandomAccessDocument.position] to the supplied [position] - */ - fun seek(position : Long) { - this.position = position - } - - /** - * This increments [position] by [amount] - */ - fun skipBytes(amount : Long) { - this.position += amount - } - - /** - * This closes [parcelFileDescriptor] so this class doesn't leak file descriptors - */ - fun close() { - parcelFileDescriptor.close() - } -}