mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-17 15:00:47 +02:00
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:
1 parent
049784d0f8
commit
3062909209
8 files changed
+1363
-650
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user