#Launcher
Launcher (啓動器) 是一類非常有用的工具,這類工具的意義就在於設置好特定的環境以特定的參數啓動特定的進程。
很多軟件也用到了 launcher, 比如 Chrome,還有 Android Studio, 在 Windows 平臺上,可見的是 studio.exe,
事實上,Android Studio 是基於 Intellij IDEA 開發的,IDE 代碼是基於 Java 的,所謂的 studio.exe 其實就是個啓動器,加載 jvm.dll 罷了。
IDEA WinLauncher 在 Github 上可以看到源碼: WinLauncher
Manifest 清單是一類記錄程序運行需求的文件,比如 Chrome 就有啓動清單:
<Application>
<VisualElements
DisplayName='Google Chrome'
Logo='46.0.2490.80\VisualElements\Logo.png'
SmallLogo='46.0.2490.80\VisualElements\SmallLogo.png'
ForegroundText='light'
BackgroundColor='#323232'>
<DefaultTile ShowName='allLogos'/>
<SplashScreen Image='46.0.2490.80\VisualElements\splash-620x300.png'/>
</VisualElements>
</Application>
Chrome 的清單主要是設置了啓動畫面以及 Logo 信息。
從 Windows XP 開啓,Windows 引入了 manifest 文件,格式基於 XML,記錄了一些 依賴組件,隔離信息等,比如下面的一個清單, requestedPrivileges 表示運行的權限,asInvoker 表示同父進程一樣,如果是 requireAdministrator,如果父進程不是管理員則需觸發 UAC 提權。 而 dpiAware 則是 DPI 縮放所需,實際上很多 Windows 應用開發商對這個一無所知。assemblyIdentity 則是一些依賴組件的信息了, 這裏要求一些基本的控件,如 MessageBox Button, Edit, Combbox 等需要支持 Vista 以後的風格。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<noInherit></noInherit>
<assemblyIdentity processorArchitecture="*" type="win32" name="Force.Charlie.OSChina.Robot" version="1.0.0.0"></assemblyIdentity>
<description>Robot</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"></requestedExecutionLevel>
</requestedPrivileges>
</security>
</trustInfo>
<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<dpiAware>true</dpiAware>
</asmv3:windowsSettings>
</asmv3:application>
<dependency optional="yes">
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.1.0" publicKeyToken="6595b64144ccf1df" language="*" processorArchitecture="*"></assemblyIdentity>
</dependentAssembly>
</dependency>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!--The ID below indicates application support for Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"></supportedOS>
</application>
</compatibility>
</assembly>
還有 .NET 應用程序,常常帶有 app.exe.config 這類應該說是混合清單了,.config 文件除了保留了清單功能,(比如這裏有指定 .NET 版本) 還支持配置信息,應用程序可以通過 ConfigurationManager 讀取配置信息。
<?xml version ="1.0"?>
<configuration>
<!--
Mixed mode assemblies (C++/CLI) will not load into a newer CLR by default. Expression disables this
so user projects can load when they have mixed mode dependencies that target older frameworks.
-->
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/>
</startup>
<runtime>
<designerNamespaceResolution enabled="true" />
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="PublicAssemblies;PrivateAssemblies" />
</assemblyBinding>
</runtime>
</configuration>
ConfigurationManager
Using System.Configuration.ConfigurationManager Example (C#)
##LD 補全的 Launcher 一般而言,Linux 進程依賴的 so 文件,如果不是通過 dlopen 動態加載的,都需要放到默認的 library 目錄,也就是 /usr/lib, /usr/local/lib, 或者是通過 export 命令設置 LD PATH,然後從 Shell 或 Shell 腳本啓動進程。
export LD_LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/lib
比如 google chrome , p4merge 等大多是寫一個 launcher 腳本。新的啓動器基於 C++ 實現,使用 TOML 格式文件作爲清單文件。
###TOML 格式清單 TOML (Tom's Obvious, Minimal Language) 是 Github 聯合創始人 Tom Preston-Werner 設計的一種極簡的配置文件,格式類似於 ini, 但比 ini 嚴格, 支持整數,浮點,字符串,數組,布爾值,表格,時間日期。解析起來非常方便。主頁 Github TOML
TOML 格式清單如下:
#launcher.manifest
# OWNERDIR is process image directory
[Launcher]
LibraryPath="/opt/boost/lib"
Path="${OWNERDIR}/../bin"
Binary="launcher_child"
這裏只設置了 LibraryPath Path Binary 。
###清單的環境變量解析 在上面的清單中,Path="${OWNERDIR}/../bin", 這需要解析,OWNERDIR 代表一個環境變量,這裏是內置的,表示程序 launcher 自身的目錄。 環境變量的解析如下:
/**
*
*
**/
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <unordered_map>
bool GetProcessImageFilePath(char *buffer, size_t bufSize) {
auto sz = readlink("/proc/self/exe", buffer, bufSize - 1);
if (sz == 0)
return false;
buffer[sz] = 0;
return true;
}
bool GetProcessImageFileFolder(char *buffer, size_t bufSize) {
auto sz = readlink("/proc/self/exe", buffer, bufSize - 1);
if (sz == 0)
return false;
buffer[sz] = 0;
while (--sz) {
if (buffer[sz] == '/') {
if (sz == 0)
buffer[1] = 0;
else
buffer[sz] = 0;
return true;
}
}
buffer[1] = 0;
return true;
}
/*
* Match Resolve and Replace profile support environment value
*/
enum KEnvStateMachine : int {
kClearReset = 0,
kEscapeAllow = 1,
kMarkAllow = 2,
kBlockBegin = 3,
kBlockEnd = 4
};
class EnvironmentValues {
private:
std::unordered_map<std::string, std::string> ev;
EnvironmentValues() {
char buffer[4096];
if (GetProcessImageFileFolder(buffer, 4096)) {
ev.insert(
std::pair<std::string, std::string>("OWNERDIR", std::string(buffer)));
}
ev.insert(std::pair<std::string, std::string>("SEPARATOR", "/"));
ev.insert(std::pair<std::string, std::string>("TMP", "/tmp"));
}
public:
bool Push(std::string key, std::string value, bool over = false) {
if (over) {
ev[key]=value;
}else{
if (ev.find(key) != ev.end())
return false;
ev.insert(std::pair<std::string, std::string>(key, value));
}
return true;
}
EnvironmentValues &operator=(const EnvironmentValues &) = delete;
EnvironmentValues(const EnvironmentValues &) = delete;
static EnvironmentValues &Instance() {
static EnvironmentValues env;
return env;
}
const decltype(ev) &get() { return ev; }
};
// ${HOME}/${USER}/sss/dir
bool OverwriterEnvironmentKv(std::string k, std::string v) {
return EnvironmentValues::Instance().Push(k, v, true);
}
bool PushEnvironmentKv(std::string k, std::string v) {
return EnvironmentValues::Instance().Push(k, v);
}
static bool ResolveEnvironment(const std::string &k, std::string &v) {
for (auto &i : EnvironmentValues::Instance().get()) {
if (i.first.compare(k) == 0) {
v = i.second;
return true;
}
}
const char *s = nullptr;
if ((s = getenv(k.c_str())) != nullptr) {
v = s;
return true;
}
return false;
}
bool BaseEnvironmentExpend(std::string &va) {
if (va.empty())
return false;
std::string ns, ks;
auto p = va.c_str();
auto n = va.size();
int pre = 0;
size_t i = 0;
KEnvStateMachine state = kClearReset;
for (; i < n; i++) {
switch (p[i]) {
case '`': {
switch (state) {
case kClearReset:
state = kEscapeAllow;
break;
case kEscapeAllow:
ns.push_back('`');
state = kClearReset;
break;
case kMarkAllow:
state = kEscapeAllow;
ns.push_back('$');
break;
case kBlockBegin:
continue;
default:
ns.push_back('`');
continue;
}
} break;
case '$': {
switch (state) {
case kClearReset:
state = kMarkAllow;
break;
case kEscapeAllow:
ns.push_back('$');
state = kClearReset;
break;
case kMarkAllow:
ns.push_back('$');
state = kClearReset;
break;
case kBlockBegin:
case kBlockEnd:
default:
ns.push_back('$');
continue;
}
} break;
case '{': {
switch (state) {
case kClearReset:
case kEscapeAllow:
ns.push_back('{');
state = kClearReset;
break;
case kMarkAllow: {
state = kBlockBegin;
pre = i;
} break;
case kBlockBegin:
ns.push_back('{');
break;
default:
continue;
}
} break;
case '}': {
switch (state) {
case kClearReset:
case kEscapeAllow:
ns.push_back('}');
state = kClearReset;
break;
case kMarkAllow:
state = kClearReset;
ns.push_back('$');
ns.push_back('}');
break;
case kBlockBegin: {
ks.assign(&p[pre + 1], i - pre - 1);
std::string v;
if (ResolveEnvironment(ks, v))
ns.append(v);
state = kClearReset;
} break;
default:
continue;
}
} break;
default: {
switch (state) {
case kClearReset:
ns.push_back(p[i]);
break;
case kEscapeAllow:
ns.push_back('`');
ns.push_back(p[i]);
state = kClearReset;
break;
case kMarkAllow:
ns.push_back('$');
ns.push_back(p[i]);
state = kClearReset;
break;
case kBlockBegin:
default:
continue;
}
} break;
}
}
va = ns;
return true;
}
###啓動器的實現
啓動器啓動後,查找清單文件,清單文件文件名爲 launcher.manifest , 要作爲其他進程的啓動器,只需要重命名和修改清單文件即可。
Launcher 隨後解析清單文件,並讀取 LibraryPath, Path, Binray 等屬性,設置好環境變量,最後通過 execvp 啓動進程,輸入的參數就是啓動器的全部參數。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <string>
#include <sys/types.h>
#include <dirent.h>
#include <iostream>
#include <cpptoml.h>
bool GetProcessImageFilePath(char *buffer, size_t bufSize);
bool BaseEnvironmentExpend(std::string &va);
bool AppendEnvironment(const char *name, const char *value,
bool front = false) {
if (name == nullptr || value == nullptr)
return false;
const char *ev = nullptr;
if ((ev = getenv(name)) == nullptr) {
setenv(name, value, 1);
return true;
}
auto l = strlen(value) + strlen(ev) + 2;
auto s = new char[l];
if (front) {
strcpy(s, value);
strcat(s, ":");
strcat(s, ev);
} else {
strcpy(s, ev);
strcat(s, ":");
strcat(s, value);
}
setenv(name, s, 1);
delete[] s;
return true;
}
bool Pathcombine(const char *folder, const char *path, std::string &cmd) {
if (folder == nullptr || path == nullptr)
return false;
if (*path == '/') {
if (access(path, R_OK) != 0)
return false;
cmd = path;
return true;
}
cmd = std::string(folder);
if (cmd.back() != '/' && *path != '/')
cmd.push_back('/');
cmd.append(path);
if (access(cmd.c_str(), R_OK) == 0)
return true;
return false;
}
bool PathRemoveFileSpec(char *dir) {
auto sz = strlen(const_cast<const char *>(dir));
while (--sz) {
if (dir[sz] == '/') {
if (sz == 0)
dir[1] = 0;
else
dir[sz] = 0;
return true;
}
}
dir[1] = 0;
return true;
}
//// launcher must find default
bool SearchManifest(std::string &cmd) {
char selfPath[4096];
if (!GetProcessImageFilePath(selfPath, 4096))
return false;
std::string manifest = std::string(selfPath) + std::string(".manifest");
if (!PathRemoveFileSpec(selfPath)) {
std::cout << "cannot resolve path " << selfPath << std::endl;
return false;
}
if (access(manifest.c_str(), R_OK) != 0) {
std::cerr << "Not found launcher manifest, path is: " << manifest
<< std::endl;
return false;
}
std::shared_ptr<cpptoml::table> g;
try {
g = cpptoml::parse_file(manifest);
} catch (const cpptoml::parse_exception &e) {
std::cerr << "Failed to parse manifest: " << manifest << ": " << e.what()
<< std::endl;
return false;
}
auto insider = [&](const char *key, const char *evar) {
if (!g->contains_qualified(key))
return;
auto s = g->get_qualified(key)->as<std::string>()->get();
if (!BaseEnvironmentExpend(s)) {
std::cout << "cannot resolve env: " << s << std::endl;
return;
}
if (!AppendEnvironment(evar, s.c_str(), true))
std::cout << "cannot append " << s << " to env " << evar << std::endl;
// std::cout<<"Append success "<<s<<" to env "<<evar<<std::endl;
// std::cout<<getenv(evar)<<std::endl;
};
insider("Launcher.Path", "PATH");
insider("Launcher.LibraryPath", "LD_LIBRARY_PATH");
if (!g->contains_qualified("Launcher.Binary"))
return false;
auto sbinary = g->get_qualified("Launcher.Binary")->as<std::string>()->get();
if (!BaseEnvironmentExpend(sbinary))
return false;
if (!Pathcombine(selfPath, sbinary.c_str(), cmd)) {
std::cout << "Cannot found path: " << cmd << std::endl;
return false;
}
return true;
}
int main(int argc, char *const argv[]) {
std::string cmd;
if (!SearchManifest(cmd))
return -1;
auto ret = execvp(cmd.c_str(), argv);
// std::cout<<"Command is: "<<cmd<<std::endl;
std::cout << "Subprocess result is: " << ret << std::endl;
return 0;
}
##Java Service Native Launcher 由於工作需要,我曾經寫過一個 Shell 的 Java 服務啓動器。如果不用第三方啓動器,直接使用 JVM 官方啓動器 java, 需要輸入很長的命令。比如運行 hello.jar 並傳遞參數,如下:
java -jar hello.jar world
如果運行的是一些服務,則可能是這樣的:
java -server -Xms512m -Xmx512m -jar com.fuck.service.jar --poolsize=24 --ip=192.168.1.12
很多時候,Java 開發者會使用 shell(batch) 腳本,或者第三方啓動器來規避這些麻煩。
###Java Service Manifest
絕大多數人並不喜歡冗長的命令,我也不例外。
在設計 Java Launcher 的時候,我採用 TOML 格式作爲啓動器清單格式,文件格式如下
#Launcher
# OWNERDIR is process image directory
[Runing]
# limit 16 bytes
Title="JSrv"
IsService=true
[Runtime]
JDKPath="/opt/jdk"
Package="${OWNERDIR}/app.jar"
VMOptions=["-Xms512m","-Xmx512m"]
ClassPath=["vendors"]
[Service]
IsDaemon=false
# default -s --signal support stop restart
EnableSignal=true
PidFile="/tmp/jsrv.pids"
Stdout="/opt/jsrv.stdout.log"
Stderr="/opt/jsrv.stderr.log"
DefaultArgs=["--config","config.example","--show-config"]
在這個清單文件中,我設計了兩種模式,第一個是基礎模式,也就是 IsService 爲 false 的時候,這個時候,程序只會讀取 Runtime 一節 的所有配置信息。
- JDKPath 讀取 jdk 所在目錄,並查找到 libjvm.so 的位置。
- Package 是運行的 Jar 包的路徑,也就是 MainClass 所在的Jar 包。
- VMoptions 是 Java 虛擬機 運行參數, ClassPath 是 Jar 包所在目錄。
在 基礎模式中,所有輸入的參數除了 Argv~0, 其他的參數都會被傳遞給 Java MainClass 。
jsrv helloworld
假如 Package 是 hello.jar, 等價於:
java -Xms512m -Xmx512m -jar hello.jar helloworld
另一種是 服務模式 ,這種模式並不支持從命令行輸入參數。當 IsService 爲 true 時,啓動器讀取 Service 節的信息.
- IsDaemon 表示啓動器是否以 守護進程的模式運行
- EnableSignal 表示開啓 -s stop -s restart 參數
- PidFile 是記錄守護進程 pid 的臨時文件
- Stdout 是 stdout 重定向。
- Stderr 是 stderr 重定向
- DefaultArgs 這個數組是要傳給 MainClass 的參數。
啓動一個進程作爲守護進程時,你需要將日誌輸出到文件。
一般而言,啓動一個守護進程,先需要 fork 出一個子進程,然後退出父進程。 通過 setsid() 函數調用,讓 init 進程收養它,使用 umask 設置默認權限,後面重定向標準輸入輸出,關閉文件句柄即可.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <signal.h>
#include <stdarg.h>
#include <string.h>
#include <fcntl.h>
int create_daemon() {
int fd;
switch (fork()) {
case -1:
printf("fork() failed\n");
return -1;
case 0:
break;
default:
exit(0);
}
if (setsid() == -1) {
return -1;
}
umask(0);
fd = open("/dev/null", O_RDWR);
if (fd == -1) {
// open /dev/null failed
return -1;
}
if (dup2(fd, STDIN_FILENO) == -1) {
// dup2(STDIN) failed
return -1;
}
if (dup2(fd, STDOUT_FILENO) == -1) {
// dup2(STDOUT) failed
return -1;
}
if (fd > STDERR_FILENO) {
if (close(fd) == -1) {
return -1;
}
}
return 0;
}
如果在 Windows 上運行一個守護進程,除了作爲一個 Windows Service 還可以通過 WinMain 啓動,在後臺運行,如果從 main 啓動, 就需要關閉控制檯,代碼如下:
#include <cstdio>
#include <Windows.h>
int create_daemon()
{
//do some thing and set io
FILE *fp;
if(freopen_s(&fp,"nul","w+b",stdout)!=0)
return -1;
fclose(fp);
if(freopen_s(&fp,"nul","w+b",stderr)!=0)
return -1;
fclose(fp);
FreeConsole();
return 0;
}
啓動器的處理流程可以借鑑上面的 LD 補全 啓動器,在讀取清單時,解析完 Runtime 一節後,讀取 Runing.IsService 的信息,
如果不爲真,則返回,這時候執行的是基礎模式,否則,繼續讀取 Service 一節的信息。
返回值如下:
enum KLauncherChannel {
kBasicVMRuntime = 0,
kServiceVMRuntime = 1,
kUnknownVMRuntime
};
如果是服務模式還需要解析命令行參數,參數幫助信息如下:
static const char *kUsage =
"Usage: %s [options] <input> \n"
" Flags:\n"
" -h [-?|--help]\tprint jsrv usage information and exit\n"
" -v [--version]\tprint jsrv version and exit\n"
" -s [--signal]\tsend a signal to jsrv,argument: stop|restart "
"\n";
解析完畢後,最後啓動 JVM,執行代碼並沒有多少不同,執行 MainClass 的函數分別如下:
int Exe(int Argc, char **Argv); /// Base mode
int Exe(std::vector<std::string> Args); /// Service mode
###JVM 的啓動流程 各個平臺上的 java 以及 Windows 平臺的 javaw 依然也只是 JVM 的一個啓動器,這類程序需要通過調用 jvm.dll 或者 libjvm.so 導出的 函數來啓動 JVM。
清單中設置了 JDKPath 信息,在 Linux 中,以 jdk8 爲例,64位系統 libjvm.so 是 $JDKPath/jre/lib/amd64/server/libjvm.so, 如果找不到 libjvm.so, 啓動器還會從 /usr/lib/libjvm.so 。
找到 libjvm.so 後,可以使用 dlopen 打開一個動態庫句柄,dlsym 查找 Symbol, 獲得函數地址,在 Windows 中 分別是 LoadLibrary 和 GetProcAddress
函數大致如下:
typedef jint (*VMInitMethord)(JavaVM **pvm, JNIEnv **env, void *args);
static bool SearchJDKPath(const std::string &jdkPath, std::string &libjvm) {
#ifdef __x86_64__
libjvm = jdkPath + "/jre/lib/amd64/server/libjvm.so";
#else
libjvm = jdkPath + "/jre/lib/server/libjvm.so";
#endif
if (access(libjvm.c_str(), X_OK) == 0)
return true;
// JDK9
#ifdef __x86_64__
libjvm = jdkPath + "/lib/amd64/server/libjvm.so";
#else
libjvm = jdkPath + "/lib/server/libjvm.so";
#endif
if (access(libjvm.c_str(), X_OK) == 0)
return true;
if (access("/usr/lib/libjvm.so", X_OK) != 0)
return false;
libjvm = "/usr/lib/libjvm.so";
return true;
}
bool VMRuntime::VMRuntimeBind() {
std::string libjvm;
if (!SearchJDKPath(rc_.jdkdir, libjvm)) {
std::cerr << "Cannot found libjvm.so, jdk path is: " << rc_.jdkdir
<< std::endl;
return false;
}
if ((vmHandle = dlopen(libjvm.c_str(), RTLD_NOW + RTLD_GLOBAL)) == nullptr) {
std::cerr << "Failure: dlopen open libjvm.so failed. " << std::endl;
return false;
}
if ((vmcall = (VMInitMethord)dlsym(vmHandle, "JNI_CreateJavaVM")) ==
nullptr) {
std::cerr << "Failure: cannot resolve JNI_CreateJavaVM !" << std::endl;
dlclose(vmHandle);
vmHandle = nullptr;
return false;
}
return true;
}
成功得到 JNI_CreateJavaVM 函數指針後,就該設置 JavaVMOption 類型變量,上面的 清單中的 VMOptions 要依次綁定到 JavaVMOption, ClassPath 目錄下的 jar 包,以及 Package 要綁定到 -Djava.class.path 上, 然後調用 JNI_CreateJavaVM 函數指針,獲得一個 JavaVM 對象和 JNIEnv 對象。
Jar 包是基於 zip 格式的文件,內部的 META-INF/MANIFEST.MF 這一清單文件記錄了 清單版本,Main-Class 以及 Class-Path, 可以從 Jar 包裏面讀取
MainClass 。
我使用了 Github 用戶 slyfoxza slyfoxza/minecraftd 的 JarReader來獲取
MainClass,不過遺憾的是,通過 JarReader 得到的 MainClass 是用 '.' 區分的,而 FindClass 需要 '/' 區分。 我是用了一個幫助函數來替換所有的 點。
static void Replace(std::string &s, char c1, char c2) {
for (auto &c : s) {
if (c == c1)
c = c2;
}
}
使用 env->FindClass 獲取 MainClass, 類型爲 jclass, 使用 env->GetStaticMethodID 獲取 MainClass 的方法 main, 後面就是將 參數轉成 JVM String[] 類型 隨後調用靜態方法即可, 大致如下:
int VMRuntime::Exe(std::vector<std::string> Args) {
if (!InitVMEnv()) {
std::cout << "Initialize VM failed !" << std::endl;
return 1;
}
JarReader jarReader(rc_.package);
auto mainClassString = jarReader.getMainClassName();
Replace(mainClassString, '.', '/');
jclass mainClass = env->FindClass(mainClassString.c_str());
if (!mainClass) {
std::cout << __LINE__ << " Cannot find MainClass: " << mainClassString
<< std::endl;
return 2;
}
jmethodID mainMethod =
env->GetStaticMethodID(mainClass, "main", "([Ljava/lang/String;)V");
if (!mainMethod) {
std::cout << __LINE__ << "Cannot GetStaticMetodID " << std::endl;
return 3;
}
jclass stringClass = env->FindClass("java/lang/String");
jobjectArray args = env->NewObjectArray(Args.size(), stringClass, NULL);
int i = 0;
for (auto &Arg : Args) {
jstring jni_arg = env->NewStringUTF(Arg.c_str());
env->SetObjectArrayElement(args, i++, jni_arg);
env->DeleteLocalRef(jni_arg);
}
env->CallStaticVoidMethod(mainClass, mainMethod, args);
// env->ExceptionDescribe();
jthrowable exc = env->ExceptionOccurred();
if (exc) {
env->ExceptionDescribe();
env->ExceptionClear();
jclass newExcCls = env->FindClass("java/lang/IllegalArgumentException");
if (!newExcCls) {
env->ThrowNew(newExcCls, "Throw Exception");
}
return 4;
}
return 0;
}
程序退出的時候需要析構這些對象。
實際上一個大致的 JVM Launcher 也就這樣了。