refactor(jnivm): resolve method names once and log calls with no handler

GetMethodID maps each name to a JniMethod enum, so dispatch compares
numbers instead of strings. Calls that reach no handler print one
"no handler for" line per class, method and signature. Unknown
gameActivity_ callbacks are recorded on the CallVoidMethod path too.
The FMOD write path reuses a per-thread buffer instead of allocating.
This commit is contained in:
vii committed 2026-10-15 01:31:02 -05:00
1 parent 049784d0f8
commit 3062909209
8 files changed
+1363 -650

No files matched your search

+7
View File
@@ -17,6 +17,13 @@ Release notes on GitHub are taken from the matching section here.
- `MOCKTAIL_ASSET_TRACE=1` now also prints the files Roblox asks for and does not get, as `[asset] miss`.
- Arch Linux: `nightcap-git` is on the AUR and builds the current `main` branch. Thanks to @dim-ghub, who
maintains it.
- The log now names each Roblox Java call that Nightcap has no handler for, once per call, as
`[JNI] no handler for <class>.<method><signature>`. This shows what a new Roblox version needs.
### Changed
- Nightcap answers Roblox's Java calls with less work per call, and the audio thread no longer allocates
memory for each block of sound it plays.
## [0.3.4] - 2026-10-11
+1
View File
@@ -252,6 +252,7 @@ add_library(mocktail_legacy_jnivm STATIC
src/jnivm/jni_references.cc
src/jnivm/jni_fields.cc
src/jnivm/jni_strings.cc
src/jnivm/jni_method_names.cc
)
add_library(Mocktail::LegacyJni ALIAS mocktail_legacy_jnivm)
target_include_directories(mocktail_legacy_jnivm PUBLIC include)
+18
View File
@@ -0,0 +1,18 @@
#include "jni_method_names.h"
#include <unordered_map>
namespace jnivm::internal {
JniMethod LookupJniMethod(std::string_view name) {
static const auto* const kMethods =
new std::unordered_map<std::string_view, JniMethod>{
#define MOCKTAIL_JNI_METHOD_ENTRY(id, text) {text, JniMethod::id},
MOCKTAIL_JNI_METHOD_NAMES(MOCKTAIL_JNI_METHOD_ENTRY)
#undef MOCKTAIL_JNI_METHOD_ENTRY
};
const auto it = kMethods->find(name);
return it == kMethods->end() ? JniMethod::kUnknown : it->second;
}
} // namespace jnivm::internal
+266
View File
@@ -0,0 +1,266 @@
#ifndef MOCKTAIL_JNIVM_JNI_METHOD_NAMES_H_
#define MOCKTAIL_JNIVM_JNI_METHOD_NAMES_H_
#include <cstdint>
#include <string_view>
namespace jnivm::internal {
// Every Java method name the JNI dispatch matches on. GetMethodID and
// GetStaticMethodID resolve a name to its entry once, so dispatch compares
// enum values. A name that is not listed resolves to JniMethod::kUnknown.
#define MOCKTAIL_JNI_METHOD_NAMES(X) \
X(kA, "a") \
X(kApply, "apply") \
X(kAttachObserver, "attachObserver") \
X(kBootstrapTheApp, "bootstrapTheApp") \
X(kCheckInit, "checkInit") \
X(kClose, "close") \
X(kCommit, "commit") \
X(kConnect, "connect") \
X(kConstructor, "<init>") \
X(kContains, "contains") \
X(kCurrentActivity, "currentActivity") \
X(kCurrentTimeMillis, "currentTimeMillis") \
X(kD, "d") \
X(kDeleteCurrentUserValues, "deleteCurrentUserValues") \
X(kDeleteSecureValue, "deleteSecureValue") \
X(kDeleteUserValues, "deleteUserValues") \
X(kDetachObserver, "detachObserver") \
X(kDispose, "dispose") \
X(kDoSubscribeProtocolMethodResponseRaw, "doSubscribeProtocolMethodResponseRaw") \
X(kEdit, "edit") \
X(kEnableBuiltInAEC, "enableBuiltInAEC") \
X(kEnableBuiltInAGC, "enableBuiltInAGC") \
X(kEnableBuiltInNS, "enableBuiltInNS") \
X(kF, "f") \
X(kFindClass, "findClass") \
X(kForName, "forName") \
X(kG, "g") \
X(kGameActivityHideKeyboard, "gameActivity_hideKeyboard") \
X(kGameActivityOnAppReady, "gameActivity_onAppReady") \
X(kGameActivityOnDidLogInReceived, "gameActivity_onDidLogInReceived") \
X(kGameActivityOnDidLogOutReceived, "gameActivity_onDidLogOutReceived") \
X(kGameActivityOnDidSignUp, "gameActivity_onDidSignUp") \
X(kGameActivityOnDidSwitchAccountReceived, "gameActivity_onDidSwitchAccountReceived") \
X(kGameActivityOnEngineInitialized, "gameActivity_onEngineInitialized") \
X(kGameActivityOnExperienceStart, "gameActivity_onExperienceStart") \
X(kGameActivityOnExperienceStop, "gameActivity_onExperienceStop") \
X(kGameActivityOnFlagsFailed, "gameActivity_onFlagsFailed") \
X(kGameActivityOnFlagsLoaded, "gameActivity_onFlagsLoaded") \
X(kGameActivityOnGameLoaded, "gameActivity_onGameLoaded") \
X(kGameActivityOnLuaAppDidReturn, "gameActivity_onLuaAppDidReturn") \
X(kGameActivityOnLuaTextBoxChanged, "gameActivity_onLuaTextBoxChanged") \
X(kGameActivityOnLuaTextBoxPropertyChanged, "gameActivity_onLuaTextBoxPropertyChanged") \
X(kGameActivityOnMotionEventListening, "gameActivity_onMotionEventListening") \
X(kGameActivityOnRestartLuaApp, "gameActivity_onRestartLuaApp") \
X(kGameActivityOnScanQrCode, "gameActivity_onScanQrCode") \
X(kGameActivityOnScreenOrientationChanged, "gameActivity_onScreenOrientationChanged") \
X(kGameActivityOnScreenshotReady, "gameActivity_onScreenshotReady") \
X(kGameActivitySetAppUpgradeStatus, "gameActivity_setAppUpgradeStatus") \
X(kGameActivityShowKeyboard, "gameActivity_showKeyboard") \
X(kGameDidLeave, "gameDidLeave") \
X(kGet, "get") \
X(kGetAbsolutePath, "getAbsolutePath") \
X(kGetActivity, "getActivity") \
X(kGetAlternateName, "getAlternateName") \
X(kGetAndroidApiVersion, "getAndroidApiVersion") \
X(kGetApiBaseUrl, "getApiBaseUrl") \
X(kGetApiGatewayUrl, "getApiGatewayUrl") \
X(kGetApiVersion, "getApiVersion") \
X(kGetApplicationContext, "getApplicationContext") \
X(kGetApplicationInfo, "getApplicationInfo") \
X(kGetAppVersion, "getAppVersion") \
X(kGetAssetManager, "getAssetManager") \
X(kGetAssets, "getAssets") \
X(kGetBaseContext, "getBaseContext") \
X(kGetBaseURL, "getBaseURL") \
X(kGetBaseUrl, "getBaseUrl") \
X(kGetBrand, "getBrand") \
X(kGetBufferSizeInFrames, "getBufferSizeInFrames") \
X(kGetBuildId, "getBuildId") \
X(kGetBuildRelease, "getBuildRelease") \
X(kGetBuildType, "getBuildType") \
X(kGetBytes, "getBytes") \
X(kGetCacheDir, "getCacheDir") \
X(kGetCanonicalPath, "getCanonicalPath") \
X(kGetClassLoader, "getClassLoader") \
X(kGetClientSettingsUrl, "getClientSettingsUrl") \
X(kGetContext, "getContext") \
X(kGetCookie, "getCookie") \
X(kGetCurrentText, "getCurrentText") \
X(kGetCurrentTextBoxInfo, "getCurrentTextBoxInfo") \
X(kGetCurrentUser, "getCurrentUser") \
X(kGetDecorView, "getDecorView") \
X(kGetDefaultDisplay, "getDefaultDisplay") \
X(kGetDensityDpi, "getDensityDpi") \
X(kGetDevice, "getDevice") \
X(kGetDeviceManufacturer, "getDeviceManufacturer") \
X(kGetDeviceModel, "getDeviceModel") \
X(kGetDeviceStaticParams, "getDeviceStaticParams") \
X(kGetDisplay, "getDisplay") \
X(kGetDisplayName, "getDisplayName") \
X(kGetExternalFilesDir, "getExternalFilesDir") \
X(kGetFilesDir, "getFilesDir") \
X(kGetGameLocale, "getGameLocale") \
X(kGetHeight, "getHeight") \
X(kGetHint, "getHint") \
X(kGetHolder, "getHolder") \
X(kGetId, "getId") \
X(kGetImplementation, "getImplementation") \
X(kGetInstallerPackageName, "getInstallerPackageName") \
X(kGetInt, "getInt") \
X(kGetLastLoggedInUser, "getLastLoggedInUser") \
X(kGetLastLoggedInUserId, "getLastLoggedInUserId") \
X(kGetLastRaw, "getLastRaw") \
X(kGetLocale, "getLocale") \
X(kGetManufacturer, "getManufacturer") \
X(kGetMessageBus, "getMessageBus") \
X(kGetMessageBusConnection, "getMessageBusConnection") \
X(kGetMode, "getMode") \
X(kGetModel, "getModel") \
X(kGetNativeHelper, "getNativeHelper") \
X(kGetNativeTextBoxInfo, "getNativeTextBoxInfo") \
X(kGetOutputBlockSize, "getOutputBlockSize") \
X(kGetOutputSampleRate, "getOutputSampleRate") \
X(kGetPackageInfo, "getPackageInfo") \
X(kGetPackageManager, "getPackageManager") \
X(kGetPackageName, "getPackageName") \
X(kGetPath, "getPath") \
X(kGetPlaceholder, "getPlaceholder") \
X(kGetPlatformName, "getPlatformName") \
X(kGetPlatformSystemDialogHandler, "getPlatformSystemDialogHandler") \
X(kGetProduct, "getProduct") \
X(kGetProperty, "getProperty") \
X(kGetPublicIPv4Addresseses, "getPublicIPv4Addresseses") \
X(kGetRefreshRate, "getRefreshRate") \
X(kGetResources, "getResources") \
X(kGetRobloxLocale, "getRobloxLocale") \
X(kGetRootView, "getRootView") \
X(kGetRotation, "getRotation") \
X(kGetScreenHeight, "getScreenHeight") \
X(kGetScreenWidth, "getScreenWidth") \
X(kGetSdkVersion, "getSdkVersion") \
X(kGetSecureValue, "getSecureValue") \
X(kGetSecureValueForCurrentUser, "getSecureValueForCurrentUser") \
X(kGetSecureValueForUser, "getSecureValueForUser") \
X(kGetSettingsUrl, "getSettingsUrl") \
X(kGetSharedPreferences, "getSharedPreferences") \
X(kGetStreamMaxVolume, "getStreamMaxVolume") \
X(kGetStreamVolume, "getStreamVolume") \
X(kGetString, "getString") \
X(kGetSurface, "getSurface") \
X(kGetSystemDialogHandler, "getSystemDialogHandler") \
X(kGetSystemService, "getSystemService") \
X(kGetSystemTheme, "getSystemTheme") \
X(kGetTargetSdkVersion, "getTargetSdkVersion") \
X(kGetText, "getText") \
X(kGetTextBoxInfo, "getTextBoxInfo") \
X(kGetTheme, "getTheme") \
X(kGetUserId, "getUserId") \
X(kGetUsername, "getUsername") \
X(kGetUsers, "getUsers") \
X(kGetVideoCodecs, "getVideoCodecs") \
X(kGetWidth, "getWidth") \
X(kGetWindow, "getWindow") \
X(kGetWindowManager, "getWindowManager") \
X(kGetWWWBaseUrl, "getWWWBaseUrl") \
X(kGetWwwBaseUrl, "getWwwBaseUrl") \
X(kH, "h") \
X(kHasNext, "hasNext") \
X(kHasSystemFeature, "hasSystemFeature") \
X(kHideKeyboard, "hideKeyboard") \
X(kIdentityHashCode, "identityHashCode") \
X(kInit, "init") \
X(kInitPlayout, "initPlayout") \
X(kInitRecording, "initRecording") \
X(kIsAAudioSupported, "isAAudioSupported") \
X(kIsAudioConfigVerified, "isAudioConfigVerified") \
X(kIsAudioSourceMatchingRecordingSession, "isAudioSourceMatchingRecordingSession") \
X(kIsAvailable, "isAvailable") \
X(kIsBluetoothA2dpOn, "isBluetoothA2dpOn") \
X(kIsBluetoothOn, "isBluetoothOn") \
X(kIsBluetoothScoOn, "isBluetoothScoOn") \
X(kIsChangingConfigurations, "isChangingConfigurations") \
X(kIsCommunicationModeEnabled, "isCommunicationModeEnabled") \
X(kIsConnected, "isConnected") \
X(kIsDestroyed, "isDestroyed") \
X(kIsDeviceBlacklistedForOpenSLESUsage, "isDeviceBlacklistedForOpenSLESUsage") \
X(kIsEmpty, "isEmpty") \
X(kIsFinishing, "isFinishing") \
X(kIsLowLatencyInputSupported, "isLowLatencyInputSupported") \
X(kIsLowLatencyOutputSupported, "isLowLatencyOutputSupported") \
X(kIsProAudioSupported, "isProAudioSupported") \
X(kIsSystemThemeAvailable, "isSystemThemeAvailable") \
X(kIsValid, "isValid") \
X(kIterator, "iterator") \
X(kK1, "k1") \
X(kL0, "l0") \
X(kLength, "length") \
X(kLoadClass, "loadClass") \
X(kLowLatencyFlag, "lowLatencyFlag") \
X(kNanoTime, "nanoTime") \
X(kNativeGetCookiesForDomain, "nativeGetCookiesForDomain") \
X(kNativeGetCookiesInNetscapeFormat, "nativeGetCookiesInNetscapeFormat") \
X(kNativeGetTextBoxInfo, "nativeGetTextBoxInfo") \
X(kOnAppBridgeNotification, "onAppBridgeNotification") \
X(kOnDataModelNotificationCallback, "onDataModelNotificationCallback") \
X(kOnItemSet, "onItemSet") \
X(kOnLuaTextBoxChangedCallback, "onLuaTextBoxChangedCallback") \
X(kOnLuaTextBoxPropertyChangedCallback, "onLuaTextBoxPropertyChangedCallback") \
X(kOnSetCookie, "onSetCookie") \
X(kOpen, "open") \
X(kOpenNativeOverlay, "openNativeOverlay") \
X(kOpenWebActivity, "openWebActivity") \
X(kOptString, "optString") \
X(kOutputAAudioHeadphonesChanged, "OutputAAudioHeadphonesChanged") \
X(kProAudioFlag, "proAudioFlag") \
X(kQ, "q") \
X(kRequestOrientationAsDefault, "requestOrientationAsDefault") \
X(kRequestResponse, "requestResponse") \
X(kRestartInput, "restartInput") \
X(kRun, "run") \
X(kRunOnUiThread, "runOnUiThread") \
X(kSetAppBridgeNotificationListener, "setAppBridgeNotificationListener") \
X(kSetBaseUrl, "setBaseUrl") \
X(kSetCookie, "setCookie") \
X(kSetCookiesFromDisk, "setCookiesFromDisk") \
X(kSetCurrentUser, "setCurrentUser") \
X(kSetImeEditorInfoFields, "setImeEditorInfoFields") \
X(kSetImplementation, "setImplementation") \
X(kSetMicrophoneMute, "setMicrophoneMute") \
X(kSetRequestHandler, "setRequestHandler") \
X(kSetRequestHandlerRaw, "setRequestHandlerRaw") \
X(kSetSecureValue, "setSecureValue") \
X(kSetSecureValueForCurrentUser, "setSecureValueForCurrentUser") \
X(kSetSecureValueForUser, "setSecureValueForUser") \
X(kSetSoftKeyboardActive, "setSoftKeyboardActive") \
X(kSetState, "setState") \
X(kSetStreamVolume, "setStreamVolume") \
X(kSetWindowFlags, "setWindowFlags") \
X(kShowKeyboard, "showKeyboard") \
X(kShowLeaveAppPrompt, "showLeaveAppPrompt") \
X(kSize, "size") \
X(kStartPlayout, "startPlayout") \
X(kStartRecording, "startRecording") \
X(kStopPlayout, "stopPlayout") \
X(kStopRecording, "stopRecording") \
X(kSubscribe, "subscribe") \
X(kSubscribeProtocolMethodResponseRaw, "subscribeProtocolMethodResponseRaw") \
X(kSupportsAAudio, "supportsAAudio") \
X(kSupportsLowLatency, "supportsLowLatency") \
X(kSyncCookiesFromEngine, "syncCookiesFromEngine") \
X(kWrite, "write")
enum class JniMethod : std::uint16_t {
kUnknown = 0,
#define MOCKTAIL_JNI_METHOD_ENUMERATOR(id, name) id,
MOCKTAIL_JNI_METHOD_NAMES(MOCKTAIL_JNI_METHOD_ENUMERATOR)
#undef MOCKTAIL_JNI_METHOD_ENUMERATOR
};
JniMethod LookupJniMethod(std::string_view name);
} // namespace jnivm::internal
#endif // MOCKTAIL_JNIVM_JNI_METHOD_NAMES_H_
+823 -650
View File
File diff suppressed because it is too large. Load diff
+9
View File
@@ -11,6 +11,14 @@ target_link_libraries(jnivm_test PRIVATE
GTest::gtest_main
)
add_executable(jnivm_fmod_write_allocation_test
jnivm_fmod_write_allocation_test.cc
)
target_link_libraries(jnivm_fmod_write_allocation_test PRIVATE
Mocktail::LegacyJni
GTest::gtest_main
)
add_executable(jnivm_fields_test jnivm_fields_test.cc)
target_link_libraries(jnivm_fields_test PRIVATE
Mocktail::LegacyJni
@@ -1026,6 +1034,7 @@ target_link_libraries(headless_graphics_test PRIVATE
include(GoogleTest)
gtest_discover_tests(jnivm_test)
gtest_discover_tests(jnivm_fields_test)
gtest_discover_tests(jnivm_fmod_write_allocation_test)
gtest_discover_tests(runtime_config_test)
gtest_discover_tests(runtime_config_file_test)
gtest_discover_tests(payload_update_preflight_test)
+80
View File
@@ -0,0 +1,80 @@
// Counts heap allocations made on the audio thread by FMOD AudioDevice.write.
#include "jnivm/jnivm.h"
#include <gtest/gtest.h>
#include <cstdint>
#include <cstdlib>
#include <memory>
#include <new>
namespace {
thread_local bool g_counting = false;
thread_local std::size_t g_allocations = 0;
void* CountedAllocate(std::size_t size) noexcept {
if (g_counting) {
++g_allocations;
}
return std::malloc(size == 0 ? 1 : size);
}
} // namespace
void* operator new(std::size_t size) {
if (void* memory = CountedAllocate(size)) {
return memory;
}
throw std::bad_alloc();
}
void* operator new[](std::size_t size) { return operator new(size); }
void* operator new(std::size_t size, const std::nothrow_t&) noexcept {
return CountedAllocate(size);
}
void* operator new[](std::size_t size, const std::nothrow_t&) noexcept {
return CountedAllocate(size);
}
void operator delete(void* memory) noexcept { std::free(memory); }
void operator delete[](void* memory) noexcept { std::free(memory); }
void operator delete(void* memory, std::size_t) noexcept { std::free(memory); }
void operator delete[](void* memory, std::size_t) noexcept {
std::free(memory);
}
namespace {
struct WriteProbe {
int writes = 0;
};
TEST(FmodAudioDeviceWrite, SteadyStateWriteDoesNotAllocate) {
auto vm = std::make_shared<jnivm::VM>();
JNIEnv* env = vm->GetJNIEnv();
auto probe = std::make_shared<WriteProbe>();
jnivm::FmodAudioDeviceCallbacks callbacks;
callbacks.write = [](void* context, const void*, const std::uint8_t*,
std::size_t) {
++static_cast<WriteProbe*>(context)->writes;
return true;
};
vm->SetFmodAudioDeviceCallbacks(probe, callbacks);
jclass device_class = env->FindClass("org/fmod/AudioDevice");
jobject device = env->NewObject(
device_class, env->GetMethodID(device_class, "<init>", "()V"));
const jmethodID write = env->GetMethodID(device_class, "write", "([BI)V");
jbyteArray block = env->NewByteArray(4096);
env->CallVoidMethod(device, write, block, 4096);
g_allocations = 0;
g_counting = true;
for (int i = 0; i < 64; ++i) {
env->CallVoidMethod(device, write, block, 4096);
}
g_counting = false;
EXPECT_EQ(g_allocations, 0u);
EXPECT_EQ(probe->writes, 65);
vm->ClearFmodAudioDeviceCallbacks();
}
} // namespace
+159
View File
@@ -3211,5 +3211,164 @@ TEST_F(JniVmTest, DeletedHighIndexStringHandleReadsAsEmpty) {
EXPECT_EQ(env->GetStringLength(high), 0);
}
std::size_t CountOccurrences(const std::string &text,
const std::string &needle) {
std::size_t count = 0;
for (std::size_t at = text.find(needle); at != std::string::npos;
at = text.find(needle, at + needle.size())) {
++count;
}
return count;
}
TEST_F(JniVmTest, UnhandledCallIsReportedOncePerClassAndMethod) {
JNIEnv *env = vm_->GetJNIEnv();
jclass first = env->FindClass("example/UnhandledFirst");
jclass second = env->FindClass("example/UnhandledSecond");
const jmethodID constructor = env->GetMethodID(first, "<init>", "()V");
jobject first_object = env->NewObject(first, constructor);
jobject second_object = env->NewObject(second, constructor);
const jmethodID method = env->GetMethodID(first, "zzReportedVoid", "(I)V");
::testing::internal::CaptureStderr();
env->CallVoidMethod(first_object, method, 1);
env->CallVoidMethod(first_object, method, 2);
CallVoidMethodVForTest(env, first_object, method, 3);
env->CallVoidMethod(second_object, method, 4);
const std::string output = ::testing::internal::GetCapturedStderr();
EXPECT_EQ(CountOccurrences(output, " [JNI] no handler for "
"example/UnhandledFirst.zzReportedVoid(I)V\n"),
1u);
EXPECT_EQ(CountOccurrences(output, " [JNI] no handler for "
"example/UnhandledSecond.zzReportedVoid(I)V\n"),
1u);
}
TEST_F(JniVmTest, UnhandledCallsAreReportedForEveryResultType) {
JNIEnv *env = vm_->GetJNIEnv();
jclass cls = env->FindClass("example/UnhandledResults");
jobject object =
env->NewObject(cls, env->GetMethodID(cls, "<init>", "()V"));
const auto id = [&](const char *name, const char *sig) {
return env->GetMethodID(cls, name, sig);
};
const auto static_id = [&](const char *name, const char *sig) {
return env->GetStaticMethodID(cls, name, sig);
};
::testing::internal::CaptureStderr();
EXPECT_EQ(env->CallObjectMethod(object, id("zzObject", "()Ljava/lang/Object;")),
nullptr);
EXPECT_EQ(env->CallBooleanMethod(object, id("zzBoolean", "()Z")), JNI_FALSE);
EXPECT_EQ(env->CallIntMethod(object, id("zzInt", "()I")), 0);
EXPECT_EQ(env->CallLongMethod(object, id("zzLong", "()J")), 0);
EXPECT_EQ(env->CallFloatMethod(object, id("zzFloat", "()F")), 0.0f);
EXPECT_EQ(env->CallDoubleMethod(object, id("zzDouble", "()D")), 0.0);
EXPECT_EQ(env->CallByteMethod(object, id("zzByte", "()B")), 0);
env->CallStaticVoidMethod(cls, static_id("zzStaticVoid", "()V"));
EXPECT_EQ(env->CallStaticObjectMethod(
cls, static_id("zzStaticObject", "()Ljava/lang/Object;")),
nullptr);
EXPECT_EQ(
env->CallStaticBooleanMethod(cls, static_id("zzStaticBoolean", "()Z")),
JNI_FALSE);
EXPECT_EQ(env->CallStaticIntMethod(cls, static_id("zzStaticInt", "()I")), 0);
EXPECT_EQ(env->CallStaticLongMethod(cls, static_id("zzStaticLong", "()J")),
0);
EXPECT_EQ(env->CallStaticFloatMethod(cls, static_id("zzStaticFloat", "()F")),
0.0f);
const std::string output = ::testing::internal::GetCapturedStderr();
const std::string prefix = " [JNI] no handler for example/UnhandledResults.";
const std::string static_prefix =
" [JNI] no handler for static example/UnhandledResults.";
for (const char *method :
{"zzObject()Ljava/lang/Object;", "zzBoolean()Z", "zzInt()I", "zzLong()J",
"zzFloat()F", "zzDouble()D", "zzByte()B"}) {
EXPECT_EQ(CountOccurrences(output, prefix + method + "\n"), 1u) << method;
}
for (const char *method :
{"zzStaticVoid()V", "zzStaticObject()Ljava/lang/Object;",
"zzStaticBoolean()Z", "zzStaticInt()I", "zzStaticLong()J",
"zzStaticFloat()F"}) {
EXPECT_EQ(CountOccurrences(output, static_prefix + method + "\n"), 1u)
<< method;
}
}
TEST_F(JniVmTest, HandledAndDeliberatelyIgnoredCallsAreNotReported) {
JNIEnv *env = vm_->GetJNIEnv();
jclass cls = env->FindClass("example/HandledProbe");
jobject object =
env->NewObject(cls, env->GetMethodID(cls, "<init>", "()V"));
env->SetIntField(object, env->GetFieldID(cls, "zzCount", "I"), 7);
jclass callback_class =
env->FindClass("com/roblox/engine/jni/EngineJavaCallback2");
jobject callback = env->NewObject(
callback_class, env->GetMethodID(callback_class, "<init>", "()V"));
jclass helper_class = env->FindClass("com/roblox/client/startup/NativeHelper");
jobject helper = env->NewObject(
helper_class, env->GetMethodID(helper_class, "<init>", "()V"));
jvalue no_args[1] = {};
::testing::internal::CaptureStderr();
(void)env->CallIntMethod(object, env->GetMethodID(cls, "getScreenWidth", "()I"));
EXPECT_EQ(env->CallIntMethod(object, env->GetMethodID(cls, "getZzCount", "()I")),
7);
env->CallVoidMethod(callback,
env->GetMethodID(callback_class, "c", "()V"));
env->CallVoidMethodA(
helper, env->GetMethodID(helper_class, "gameActivity_zzLater", "()V"),
no_args);
const std::string output = ::testing::internal::GetCapturedStderr();
EXPECT_EQ(output.find("no handler for"), std::string::npos) << output;
}
TEST_F(JniVmTest, UnmatchedRunAAndFVoidCallsAreReported) {
JNIEnv *env = vm_->GetJNIEnv();
jclass cls = env->FindClass("example/UnmatchedShortNames");
jobject object =
env->NewObject(cls, env->GetMethodID(cls, "<init>", "()V"));
jvalue no_args[1] = {};
::testing::internal::CaptureStderr();
env->CallVoidMethod(object, env->GetMethodID(cls, "run", "()V"));
env->CallVoidMethod(object, env->GetMethodID(cls, "f", "()V"));
env->CallVoidMethodA(object, env->GetMethodID(cls, "a", "()V"), no_args);
const std::string output = ::testing::internal::GetCapturedStderr();
const std::string prefix =
" [JNI] no handler for example/UnmatchedShortNames.";
for (const char *method : {"run()V", "f()V", "a()V"}) {
EXPECT_EQ(CountOccurrences(output, prefix + method + "\n"), 1u) << method;
}
}
TEST_F(JniVmTest, UnknownGameActivityCallbackIsRecordedOnEveryVoidPath) {
JNIEnv *env = vm_->GetJNIEnv();
jclass helper_class = env->FindClass("com/roblox/client/startup/NativeHelper");
jobject helper = env->NewObject(
helper_class, env->GetMethodID(helper_class, "<init>", "()V"));
jvalue no_args[1] = {};
::testing::internal::CaptureStderr();
env->CallVoidMethod(
helper, env->GetMethodID(helper_class, "gameActivity_zzViaV", "()V"));
env->CallVoidMethodA(
helper, env->GetMethodID(helper_class, "gameActivity_zzViaA", "()V"),
no_args);
const std::string output = ::testing::internal::GetCapturedStderr();
EXPECT_EQ(output.find("no handler for"), std::string::npos) << output;
for (const char *name : {"gameActivity_zzViaV", "gameActivity_zzViaA"}) {
EXPECT_EQ(env->GetBooleanField(helper,
env->GetFieldID(helper_class, name, "Z")),
JNI_TRUE)
<< name;
}
}
} // namespace
} // namespace jnivm