If anyone is interested, my solution was to create a exe that simply uses the c jni interface to spawn a jvm and run my application. This also allows for telling Windows that you want to use the high performance GPU when available (on systems such as laptops that have both an integrated and non-integrated).
// WindowsOutsideLauncher.cpp : This file contains the 'main' function. Program execution begins and ends there.
//
#include <windows.h>
#include <signal.h>
#include <iostream>
#include <fstream>
#include <sstream>
#include <map>
#include <jni.h>
#include <string>
#include <vector>
#include <algorithm>
#include "zip.h"
#include "zip.c"
#include "miniz.h"
//Define constants
//These will be passed in by the compiler
//Values will come from OutsideClient to ensure they are correct
#ifndef MAIN_JAR
#define MAIN_JAR "OutsideClient.jar"
#endif
#ifndef MAIN_CLASS
#define MAIN_CLASS "io/tlf/outside/client/Main"
#endif
using namespace std;
// high performance gpu for nvidia and amd
extern "C"
{
__declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001;
__declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
}
//Vars for parsing manifest
char *manifest;
int manifestSize;
std::map <std::string, std::string> manifestProperties;
//Manifest functions
void getProperties() {
std::string manifestString = std::string(manifest, manifestSize);
std::stringstream iss(manifestString);
std::string key;
std::string value;
std::string line;
bool first = true;
while (std::getline(iss, line)) {
if (!line.empty() && *line.rbegin() == '\r') {
line.erase(line.length() - 1, 1);
}
if (line.find(' ') != 0) {
if (line.find(':') > line.length() || line.find(':') <= 0) {
break;
}
//std::cout << "Parsing line: " << line << std::endl;
if (!first) {
manifestProperties.insert(std::pair<std::string, std::string>(key, value));
}
value = line.substr(line.find(':') + 2, line.length());
key = line.substr(0, line.find(':'));
//std::cout << "Added: " << key << " : " << value << std::endl;
first = false;
} else {
//std::cout << "Line continues: " << line << std::endl;
//std::cout << "Value: " << value << std::endl;
value.append(line);
}
}
//std::cout << "Key: " << key << std::endl;
//std::cout << "Value: " << value << std::endl;
manifestProperties.insert(std::pair<std::string, std::string>(key, value));
}
void readManifest(std::string jarFile) {
size_t bufsize;
struct zip_t *zip = zip_open(jarFile.c_str(), 0, 'r');
{
std::cout << "File open" << std::endl;
zip_entry_open(zip, "META-INF/MANIFEST.MF");
{
bufsize = zip_entry_size(zip);
manifest = static_cast<char *>(calloc(sizeof(char), bufsize));
zip_entry_noallocread(zip, (void *) manifest, bufsize);
manifestSize = static_cast<int>(bufsize);
}
zip_entry_close(zip);
}
zip_close(zip);
std::cout << "File close" << std::endl;
getProperties();
std::cout << "Manifest parsed!" << std::endl;
}
void reportJvmCrash() {
ofstream myfile;
myfile.open ("crash_test.txt");
myfile << "The JVM Crashed!\n";
myfile.close();
}
void SignalHandler(int signal)
{
printf("Signal %d",signal);
throw "JVM Crashed";
}
//Main function
int main(int argc, char **argv) {
typedef void (*SignalHandlerPointer)(int);
SignalHandlerPointer previousHandler;
previousHandler = signal(SIGSEGV , SignalHandler);
SetDllDirectory("./jvm/bin/server");
SetEnvironmentVariable("JAVA_HOME", "./jvm");
SetEnvironmentVariable("OUTSIDE_EXE", argv[0]);
cout << "Launched from exe: " << argv[0] << endl;
cout << "Preparing to launch Outside Client" << endl;
JavaVM *jvm; // Pointer to the JVM (Java Virtual Machine)
JNIEnv *env; // Pointer to native interface
vector<char *> jvmArgs;
bool classpathSet = false;
char *mainClass = (char *) MAIN_CLASS;
char *jarFile;
JavaVMInitArgs vm_args; // Initialization arguments
for (int i = 1; i < argc; i++) {
cout << "JVM Option " << i << ": " << argv[i] << endl;
char *opt;
//Check if this is the -jar argument, if so we convert to a classpath argument
if (strcmp(argv[i], "-jar") == 0) {
opt = (char *) "-Djava.class.path=";
jarFile = argv[i + 1];
char *full_text;
full_text = (char *) malloc(strlen(opt) + strlen(argv[i + 1]) + 1);
strcpy(full_text, opt);
strcat(full_text, jarFile);
opt = full_text;
jvmArgs.push_back(opt);
classpathSet = true;
cout << "Jar override to classpath: " << opt << endl;
i++;
} else {
//Check if this is a -Djava.class.path override
char *search = (char *) "-Djava.class.path=";
if (strncmp(argv[i], search, strlen(search)) == 0) {
classpathSet = true;
//Get name of jar file
jarFile = (char *) malloc(strlen(argv[i]) - strlen(search) + 1);
strncpy(argv[i] + strlen(search), jarFile, strlen(argv[i]) - strlen(search));
}
//For all arguments, we will store them
opt = argv[i];
jvmArgs.push_back(opt);
}
}
//Set -XX:+UseShenandoahGC
jvmArgs.push_back("-XX:+UseShenandoahGC");
if (!classpathSet) {
//Build string for default branded jar
char *optSub = (char *) "-Djava.class.path=";
char *opt = (char *) malloc(strlen(optSub) + strlen(MAIN_JAR));
jarFile = (char *) MAIN_JAR;
strcpy(opt, optSub);
strcat(opt, MAIN_JAR);
jvmArgs.push_back(opt);
}
//Store jvm optopns
JavaVMOption *options = new JavaVMOption[jvmArgs.size()]; // JVM invocation options
for (unsigned int i = 0; i < jvmArgs.size(); i++) {
options[i].optionString = jvmArgs[i];
}
vm_args.version = JNI_VERSION_10; // minimum Java version
vm_args.nOptions = jvmArgs.size(); // number of options
vm_args.options = options;
vm_args.ignoreUnrecognized = false; // invalid options make the JVM init fail
cout << "Creating jvm" << endl;
jint rc = JNI_CreateJavaVM(&jvm, (void **) &env, &vm_args); // Build JVM
delete options; // we then no longer need the initialisation options.
if (rc != JNI_OK) {
cerr << "Failed to create jvm" << endl;
cin.get();
exit(EXIT_FAILURE);
}
cout << "JVM load succeeded";
//Read jar
readManifest(jarFile);
//Swap out the '.' for '/'
string mainClassStr = manifestProperties["Main-Class"];
std::replace(mainClassStr.begin(), mainClassStr.end(), '.', '/');
mainClass = (char *) mainClassStr.c_str();
std::cout << "Main Class: " << manifestProperties["Main-Class"] << std::endl;
jclass cls2 = env->FindClass(mainClass); // try to find the class
if (cls2 == nullptr) {
cerr << "ERROR: class not found: " << mainClass << endl;
} else { // if class found, continue
cout << "Class Main found" << endl;
jmethodID mid = env->GetStaticMethodID(cls2, "main", "([Ljava/lang/String;)V");
if (mid == nullptr)
cerr << "ERROR: Main method not found" << endl;
else {
cout << "Main found, loading outside" << endl;
jobjectArray arr = env->NewObjectArray(
0, // constructs java array of 0
env->FindClass("java/lang/String"), // Strings
env->NewStringUTF("")
); // each initialized with value "str"
try {
env->CallStaticVoidMethod(cls2, mid, arr); // call the method with the arr as argument.
env->DeleteLocalRef(arr); // release the object
} catch (char *e) {
cerr << "ERROR: JVM has crashed!" << endl;
reportJvmCrash();
}
}
}
jvm->DestroyJavaVM();
return 0;
}
Resource file: WindowsOutsideLauncher.rc
Note: You will need to modify the path to the icon relative to the resource file.
// Icons
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
128 ICON "..\\dist\\outside.ico"
Gradle file (Note: this is setup for my project, it will take some tweaks for whoever uses it)
plugins {
id 'cpp-application'
//
id "de.undercouch.download" version "$gradleDownloadPluginVersion"
}
apply from: "$rootDir/common.gradle"
ext {
exeName = "OutsideClient.exe"
mainJarMacro = rootProject.project(":modules:io.tlf.outside.client").clientJarName
mainClassMacro = rootProject.project(":modules:io.tlf.outside.client").clientMainClass
libDir = new File(projectDir, "lib")
}
task makeLibDir {
doLast {
mkdir libDir
}
}
task cleanLibDir {
doLast {
delete files(libDir)
}
}
task cloneZip(dependsOn: makeLibDir) {
doLast {
if (!file("$libDir/zip").exists()) {
exec {
workingDir "${libDir}"
commandLine 'git', 'clone', '-b', 'v0.2.0', 'https://github.com/kuba--/zip'
}
}
}
}
application {
File jdk32 = new File(libDir, 'jdk32')
//println new File(jdk32, "include").getPath()
File jdk64 = new File(libDir, 'jdk64')
//println new File(jdk64, "include").getPath()
File zip = new File(libDir, 'zip')
//cpp
baseName = exeName.replaceAll('.exe$', '') //Remove the exe from the end as the compiler adds it
targetMachines = [machines.windows.x86, machines.windows.x86_64]
binaries.configureEach(CppExecutable) { binary ->
// Define a preprocessor macro for every binary
compileTask.get().macros.put("NDEBUG", null)
// Define a compiler options
compileTask.get().compilerArgs.add '-W3'
//Set correct jdk based on build arch
def jdk
switch (compileTask.get().getTargetPlatform().get().architecture.name) {
case "x86-64":
jdk = jdk64
break
case "x86":
default:
jdk = jdk32
}
// Define toolchain-specific compiler options
if (toolChain in VisualCpp) {
//==== Compiler
compileTask.get().compilerArgs.add('/v')
compileTask.get().compilerArgs.add('/Zi')
compileTask.get().compilerArgs.add('/D _CRT_SECURE_NO_WARNINGS')
compileTask.get().compilerArgs.add("/D MAIN_JAR=$mainJarMacro")
compileTask.get().compilerArgs.add("/D MAIN_CLASS=$mainClassMacro")
compileTask.get().compilerArgs.add('-I' + new File(zip, "src").getPath())
compileTask.get().compilerArgs.add('-I' + new File(jdk, "include").getPath())
compileTask.get().compilerArgs.add('-I' + new File(jdk, "include/win32").getPath())
//==== Linker
linkTask.get().linkerArgs.add(new File(jdk, "lib/jvm.lib").getPath())
//Link DLL on demand in application. Allows launcher to select where the jre is at run time
linkTask.get().linkerArgs.add('/DELAYLOAD:jvm.dll')
linkTask.get().linkerArgs.add('Delayimp.lib')
//Hide console window
if (releaseType == 'release') {
linkTask.get().linkerArgs.add('/SUBSYSTEM:windows')
linkTask.get().linkerArgs.add('/ENTRY:mainCRTStartup')
}
def compileResources = tasks.register("compileResources${binary.name.capitalize()}", WindowsResourceCompile) { wrc ->
wrc.targetPlatform = binary.compileTask.get().targetPlatform
wrc.toolChain = binary.toolChain
wrc.includes.from file("src/main/headers")
wrc.compilerArgs.add "/v"
wrc.macros.put("OUTSIDE_VERSION_STRING", versionString)
wrc.macros.put("OUTSIDE_VERSION_ID_STRING", versionId.toString())
wrc.macros.put("OUTSIDE_EXE_NAME", binary.name)
wrc.macros.put("OUTSIDE_PRODUCT_NAME", "The Outside Engine")
wrc.macros.put("OUTSIDE_COMPANY_NAME", "Liquid Crystal Studios")
wrc.source.from fileTree(dir: "src/main/rc", includes: ["**/*.rc"])
wrc.outputDir = layout.buildDirectory.dir("${binary.name}").get().asFile
}
linkTask.get().configure {
dependsOn compileResources
source.from compileResources.map({ return fileTree(dir: it.outputDir, includes: ["**/*.res", "**/*.obj"]) })
linkerArgs.add "user32.lib"
}
}
}
}
model {
toolChains {
vc(VisualCpp) {
if (new File("C:/Program Files (x86)/Microsoft Visual Studio/2019/BuildTools").exists()) {
installDir "C:/Program Files (x86)/Microsoft Visual Studio/2019/BuildTools"
}
}
}
}
task setupJdk64() {
doLast {
File jvmZip = new File(libDir, 'jdk64.zip')
if (!jvmZip.exists()) {
download {
src 'https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17%2B35/OpenJDK17-jdk_x64_windows_hotspot_17_35.zip'
dest jvmZip
overwrite false
}
}
File jdkDir = new File(libDir, 'jdk64')
if (!jdkDir.exists()) {
copy {
from zipTree(jvmZip)
into jdkDir
}
File extracted = jdkDir.listFiles()[0] //Get the contents of the jvm folder that was created.
copy {
from extracted
into jdkDir
}
extracted.deleteDir()
}
}
}
task setupJdk32() {
doLast {
File jvmZip = new File(libDir, 'jdk32.zip')
if (!jvmZip.exists()) {
download {
src 'https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17%2B35/OpenJDK17-jdk_x86-32_windows_hotspot_17_35.zip'
dest jvmZip
overwrite false
}
}
File jdkDir = new File(libDir, 'jdk32')
if (!jdkDir.exists()) {
copy {
from zipTree(jvmZip)
into jdkDir
}
File extracted = jdkDir.listFiles()[0] //Get the contents of the jvm folder that was created.
copy {
from extracted
into jdkDir
}
extracted.deleteDir()
}
}
}
task subDist64(dependsOn: ['setupJdk64', 'assembleReleaseX86-64', ':copyDist']) {
doLast {
File jvmZip = new File(libDir, 'jdk64.zip') //TODO: Make this jre64.zip
if (!jvmZip.exists()) {
download {
src 'https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17%2B35/OpenJDK17-jdk_x64_windows_hotspot_17_35.zip'
dest jvmZip
overwrite false
}
}
File jreDir = new File(libDir, 'jre64')
if (!jreDir.exists()) {
copy {
from zipTree(jvmZip)
into jreDir
}
File extracted = jreDir.listFiles()[0] //Get the contents of the jvm folder that was created.
copy {
from extracted
into jreDir
}
extracted.deleteDir()
}
//Clean
File clientWin = new File("$buildDir\\client_win64")
//Copy client
copy {
from new File("${rootProject.buildDir}\\client_dist")
into clientWin
}
//Copy jvm
copy {
from new File(libDir, 'jre64')
into new File(clientWin, 'jvm')
}
copy {
from new File(buildDir, "exe\\main\\release\\x86-64\\$exeName")
into clientWin
}
copy {
from('src/main/dist')
into clientWin
}
}
}
task subDist32(dependsOn: ['setupJdk32', 'assembleReleaseX86', ':copyDist']) {
doLast {
File jvmZip = new File(libDir, 'jdk32.zip') //TODO: Make this jre32.zip
if (!jvmZip.exists()) {
download {
src 'https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17%2B35/OpenJDK17-jdk_x86-32_windows_hotspot_17_35.zip'
dest jvmZip
overwrite false
}
}
File jreDir = new File(libDir, 'jre32')
if (!jreDir.exists()) {
copy {
from zipTree(jvmZip)
into jreDir
}
File extracted = jreDir.listFiles()[0] //Get the contents of the jvm folder that was created.
copy {
from extracted
into jreDir
}
extracted.deleteDir()
}
//Clean
File clientWin = new File("$buildDir\\client_win32")
//Copy client
copy {
from new File("${rootProject.buildDir}\\client_dist")
into clientWin
}
//Copy jvm
copy {
from new File(libDir, 'jre32')
into new File(clientWin, 'jvm')
}
copy {
from new File(buildDir, "exe\\main\\release\\x86\\$exeName")
into clientWin
}
copy {
from('src/main/dist')
into clientWin
}
}
}
tasks.whenTaskAdded { task ->
if (task.name == 'compileReleaseX86Cpp') {
task.dependsOn setupJdk32
task.dependsOn cloneZip
}
if (task.name == 'compileDebugX86Cpp') {
task.dependsOn setupJdk32
task.dependsOn cloneZip
}
if (task.name == 'compileReleaseX86-64Cpp') {
task.dependsOn setupJdk64
task.dependsOn cloneZip
}
if (task.name == 'compileDebugX86-64Cpp') {
task.dependsOn setupJdk64
task.dependsOn cloneZip
}
}
This does not create an installer. You install directory will need to contain the following files:
- You jar file and all associated files for your application
- The exe generated to launch the jar
- The
jvm
folder containing the JVM.
With this, you develop your application like you normally would, then just use the exe to launch your application.
Example:
I hope this helps, if nothing else you might get an idea on building your own wrapper utility.
~Trevor