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.
This commit is contained in:
Billy Laws 2020-06-19 21:18:33 +01:00 committed by ◱ PixelyIon
parent dca06f2b49
commit 1bb979a7e1
7 changed files with 154 additions and 201 deletions

View File

@ -25,7 +25,8 @@ set(CMAKE_POLICY_DEFAULT_CMP0048 NEW)
include_directories(${source_DIR}/skyline) include_directories(${source_DIR}/skyline)
add_library(skyline SHARED 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/common.cpp
${source_DIR}/skyline/nce/guest.S ${source_DIR}/skyline/nce/guest.S
${source_DIR}/skyline/nce/guest.cpp ${source_DIR}/skyline/nce/guest.cpp

View File

@ -3,6 +3,7 @@
#include <csignal> #include <csignal>
#include <unistd.h> #include <unistd.h>
#include "skyline/loader/loader.h"
#include "skyline/common.h" #include "skyline/common.h"
#include "skyline/os.h" #include "skyline/os.h"
#include "skyline/jvm.h" #include "skyline/jvm.h"
@ -50,7 +51,7 @@ extern "C" JNIEXPORT void Java_emu_skyline_EmulationActivity_executeApplication(
auto romUri = env->GetStringUTFChars(romUriJstring, nullptr); auto romUri = env->GetStringUTFChars(romUriJstring, nullptr);
logger->Info("Launching ROM {}", romUri); logger->Info("Launching ROM {}", romUri);
env->ReleaseStringUTFChars(romUriJstring, romUri); env->ReleaseStringUTFChars(romUriJstring, romUri);
os.Execute(romFd, static_cast<skyline::TitleFormat>(romType)); os.Execute(romFd, static_cast<skyline::loader::RomFormat>(romType));
} catch (std::exception &e) { } catch (std::exception &e) {
logger->Error(e.what()); logger->Error(e.what());
} catch (...) { } 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) { extern "C" JNIEXPORT jfloat Java_emu_skyline_EmulationActivity_getFrametime(JNIEnv *env, jobject thiz) {
return static_cast<float>(frametime) / 100; return static_cast<float>(frametime) / 100;
} }

View File

@ -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<skyline::loader::RomFormat>(jformat);
try {
auto backing = std::make_shared<skyline::vfs::OsBacking>(fd);
switch (format) {
case skyline::loader::RomFormat::NRO:
return reinterpret_cast<jlong>(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<skyline::loader::Loader *>(instance)->nacp != nullptr;
}
extern "C" JNIEXPORT jbyteArray JNICALL Java_emu_skyline_loader_RomFile_getIcon(JNIEnv *env, jobject thiz, jlong instance) {
std::vector<skyline::u8> buffer = reinterpret_cast<skyline::loader::Loader *>(instance)->GetIcon();
jbyteArray result = env->NewByteArray(buffer.size());
env->SetByteArrayRegion(result, 0, buffer.size(), reinterpret_cast<const jbyte *>(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<skyline::loader::Loader *>(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<skyline::loader::Loader *>(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<skyline::loader::NroLoader *>(instance);
}

View File

@ -28,9 +28,8 @@ import emu.skyline.adapter.AppAdapter
import emu.skyline.adapter.AppItem import emu.skyline.adapter.AppItem
import emu.skyline.adapter.GridLayoutSpan import emu.skyline.adapter.GridLayoutSpan
import emu.skyline.adapter.LayoutType import emu.skyline.adapter.LayoutType
import emu.skyline.loader.BaseLoader import emu.skyline.loader.RomFile
import emu.skyline.loader.NroLoader import emu.skyline.loader.RomFormat
import emu.skyline.utility.RandomAccessDocument
import kotlinx.android.synthetic.main.main_activity.* import kotlinx.android.synthetic.main.main_activity.*
import kotlinx.android.synthetic.main.titlebar.* import kotlinx.android.synthetic.main.titlebar.*
import java.io.File 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 * 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 var foundCurrent = found
directory.listFiles().forEach { file -> directory.listFiles().forEach { file ->
if (file.isDirectory) { if (file.isDirectory) {
foundCurrent = addEntries(extension, loader, file, foundCurrent) foundCurrent = addEntries(extension, romFormat, file, foundCurrent)
} else { } else {
if (extension.equals(file.name?.substringAfterLast("."), ignoreCase = true)) { 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)) { if (romFile.valid()) {
val entry = loader.getAppEntry(document, file.uri) romFile.use {
val entry = romFile.getAppEntry(file.uri)
runOnUiThread { runOnUiThread {
if (!foundCurrent) { if (!foundCurrent) {
adapter.addHeader(loader.format.name) 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 { try {
runOnUiThread { adapter.clear() } 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 { runOnUiThread {
if (!foundNros) if (!foundNros)

View File

@ -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
}
}

View File

@ -10,8 +10,9 @@ import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.os.ParcelFileDescriptor
import android.provider.OpenableColumns import android.provider.OpenableColumns
import emu.skyline.utility.RandomAccessDocument import android.view.Surface
import java.io.IOException import java.io.IOException
import java.io.ObjectInputStream import java.io.ObjectInputStream
import java.io.ObjectOutputStream import java.io.ObjectOutputStream
@ -21,10 +22,10 @@ import java.util.*
/** /**
* An enumeration of all supported ROM formats * An enumeration of all supported ROM formats
*/ */
enum class RomFormat { enum class RomFormat(val format: Int){
NRO, NRO(0),
XCI, XCI(1),
NSP, NSP(2),
} }
/** /**
@ -69,7 +70,7 @@ class AppEntry : Serializable {
*/ */
var uri : Uri 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.name = name
this.author = author this.author = author
this.icon = icon 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
}
}
}

View File

@ -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()
}
}