diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 5311f4d50..ff6ae65d3 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -58,6 +58,7 @@ android {
androidResources.generateLocaleConfig = true
buildFeatures {
+ aidl = true
compose = true
prefab = true
buildConfig = true
diff --git a/app/src/androidTest/java/ru/playsoftware/j2meloader/memory/MemoryTargetProbeTest.java b/app/src/androidTest/java/ru/playsoftware/j2meloader/memory/MemoryTargetProbeTest.java
new file mode 100644
index 000000000..984093e01
--- /dev/null
+++ b/app/src/androidTest/java/ru/playsoftware/j2meloader/memory/MemoryTargetProbeTest.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ru.playsoftware.j2meloader.memory;
+
+import android.os.Process;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class MemoryTargetProbeTest {
+ private static final int MANAGED_PROBE_A = 0x5A17C0DE;
+ private static final int MANAGED_PROBE_B = 0x31C0FFEE;
+ private static volatile int managedProbe;
+
+ @Test
+ public void thoroughProbeReturnsAlignedCompleteResidentRuns() {
+ int pageSize = NativeMemoryTarget.pageSize();
+ assertTrue(pageSize > 0);
+
+ long[] runs = NativeMemoryTarget.collectResidentRuns(
+ MemoryEngineContract.SCOPE_JAVA_THOROUGH, 4096);
+ assertNotNull(runs);
+ assertTrue(MemoryEngineContract.isCompleteRunList(runs));
+ for (int index = 2; index < runs.length; index += 2) {
+ assertTrue(runs[index] > 0L);
+ assertTrue(runs[index + 1] > runs[index]);
+ assertEquals(0L, runs[index] % pageSize);
+ assertEquals(0L, runs[index + 1] % pageSize);
+ if (index > 2) {
+ assertTrue(runs[index] >= runs[index - 1]);
+ }
+ }
+ }
+
+ @Test
+ public void readCapabilityProbeVerifiesExpectedBits() {
+ long[] probe = NativeMemoryTarget.readProbe();
+ assertNotNull(probe);
+ assertEquals(2, probe.length);
+ assertTrue(NativeMemoryEngine.canReadTarget(Process.myPid(), probe[0], probe[1]));
+ assertTrue(!NativeMemoryEngine.canReadTarget(Process.myPid(), probe[0], probe[1] ^ 1L));
+ }
+
+ @Test
+ public void nativeEngineFindsAndRefinesManagedArtValue() {
+ int pageSize = NativeMemoryTarget.pageSize();
+ long[] runs = NativeMemoryTarget.collectResidentRuns(
+ MemoryEngineContract.SCOPE_JAVA_FAST, 4096);
+ assertNotNull(runs);
+ assertTrue(MemoryEngineContract.isCompleteRunList(runs));
+
+ long token = 0x4A4C4D454D544553L;
+ managedProbe = MANAGED_PROBE_A;
+ try {
+ assertEquals(MemoryEngineContract.RESULT_OK,
+ NativeMemoryEngine.configureTarget(
+ Process.myPid(), pageSize, token, runs));
+ assertEquals(MemoryEngineContract.RESULT_OK,
+ NativeMemoryEngine.startKnown(
+ MemoryEngineContract.TYPE_INT,
+ MemoryEngineContract.PREDICATE_EQUAL,
+ Integer.toString(MANAGED_PROBE_A), ""));
+ assertTrue("managed ART probe was not inside the selected Java ranges",
+ NativeMemoryEngine.resultCount() > 0L);
+
+ managedProbe = MANAGED_PROBE_B;
+ assertEquals(MemoryEngineContract.RESULT_OK,
+ NativeMemoryEngine.refineKnown(
+ MemoryEngineContract.PREDICATE_EQUAL,
+ Integer.toString(MANAGED_PROBE_B), ""));
+ assertTrue("managed ART probe did not survive direct refine",
+ NativeMemoryEngine.resultCount() > 0L);
+ } finally {
+ managedProbe = 0;
+ NativeMemoryEngine.clear();
+ }
+ }
+
+ @Test
+ public void runtimeTokenCannotBeClosedByAnOlderOwner() {
+ long[] endedToken = {0L};
+ MemoryRuntimeSession.Listener listener = token -> endedToken[0] = token;
+ MemoryRuntimeSession.addListener(listener);
+ try {
+ long token = MemoryRuntimeSession.start();
+ assertTrue(token != 0L);
+ MemoryRuntimeSession.close(token ^ 1L);
+ assertTrue(MemoryRuntimeSession.isActive(token));
+ assertEquals(0L, endedToken[0]);
+ MemoryRuntimeSession.close(token);
+ assertEquals(0L, MemoryRuntimeSession.currentToken());
+ assertEquals(token, endedToken[0]);
+ } finally {
+ MemoryRuntimeSession.close(MemoryRuntimeSession.currentToken());
+ MemoryRuntimeSession.removeListener(listener);
+ }
+ }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 777246476..333414e72 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -220,6 +220,14 @@
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="j2me_midlet_runtime" />
+
+
diff --git a/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryEngineCallback.aidl b/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryEngineCallback.aidl
new file mode 100644
index 000000000..fc9d2b3e5
--- /dev/null
+++ b/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryEngineCallback.aidl
@@ -0,0 +1,14 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
+ */
+
+package ru.playsoftware.j2meloader.memory;
+
+/** Publishes completion of an asynchronous Memory Editor engine operation. */
+oneway interface IMemoryEngineCallback {
+ void onOperationFinished(long operationId, int resultCode, long resultCount, String message);
+}
diff --git a/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryEngineService.aidl b/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryEngineService.aidl
new file mode 100644
index 000000000..58ac931e3
--- /dev/null
+++ b/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryEngineService.aidl
@@ -0,0 +1,32 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
+ */
+
+package ru.playsoftware.j2meloader.memory;
+
+import android.os.Bundle;
+import ru.playsoftware.j2meloader.memory.IMemoryEngineCallback;
+
+/** Logical API; result addresses are informational and no operation accepts a raw destination. */
+interface IMemoryEngineService {
+ Bundle getCapabilities();
+ void registerCallback(IMemoryEngineCallback callback);
+ void unregisterCallback(IMemoryEngineCallback callback);
+
+ long startKnownSearch(long runtimeToken, int scope, int valueType, int predicate,
+ String firstValue, String secondValue);
+ long startUnknownSearch(long runtimeToken, int scope, int valueType);
+ long refineKnown(long runtimeToken, int predicate, String firstValue, String secondValue);
+ long refineRelative(long runtimeToken, int predicate, int compareTarget,
+ String firstValue, String secondValue);
+ long undoSearch(long runtimeToken);
+
+ long getResultCount(long runtimeToken);
+ long[] getResultPage(long runtimeToken, int offset, int limit);
+ void clearSearch(long runtimeToken);
+ void cancelOperation(long runtimeToken);
+}
diff --git a/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryTargetBridge.aidl b/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryTargetBridge.aidl
new file mode 100644
index 000000000..9a3956636
--- /dev/null
+++ b/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryTargetBridge.aidl
@@ -0,0 +1,28 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
+ */
+
+package ru.playsoftware.j2meloader.memory;
+
+import ru.playsoftware.j2meloader.memory.IMemoryTargetCallback;
+
+/** Thin target-process bridge. Scanning and candidate ownership remain in :memory_engine. */
+interface IMemoryTargetBridge {
+ void registerTargetCallback(IMemoryTargetCallback callback);
+ void unregisterTargetCallback(IMemoryTargetCallback callback);
+ long getRuntimeToken();
+ int getTargetPid();
+ int getPageSize();
+ /** Returns [address, expectedBits] for a target-owned read-only capability probe. */
+ long[] getReadProbe(long runtimeToken);
+
+ /**
+ * Returns [runCount, truncated, start0, end0, ...]. Only resident readable/writable runs
+ * selected by the requested scope are returned.
+ */
+ long[] getResidentRuns(long runtimeToken, int scope, int maxRuns);
+}
diff --git a/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryTargetCallback.aidl b/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryTargetCallback.aidl
new file mode 100644
index 000000000..ba77d346c
--- /dev/null
+++ b/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryTargetCallback.aidl
@@ -0,0 +1,14 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
+ */
+
+package ru.playsoftware.j2meloader.memory;
+
+/** Notifies the engine that an address namespace is no longer valid. */
+oneway interface IMemoryTargetCallback {
+ void onRuntimeEnded(long runtimeToken);
+}
diff --git a/app/src/main/cpp/Android.mk b/app/src/main/cpp/Android.mk
index 1d4ec7f86..5053e7d64 100644
--- a/app/src/main/cpp/Android.mk
+++ b/app/src/main/cpp/Android.mk
@@ -1 +1 @@
-include $(call all-subdir-makefiles)
\ No newline at end of file
+include $(call all-subdir-makefiles)
diff --git a/app/src/main/cpp/memory/Android.mk b/app/src/main/cpp/memory/Android.mk
new file mode 100644
index 000000000..ec856d064
--- /dev/null
+++ b/app/src/main/cpp/memory/Android.mk
@@ -0,0 +1,17 @@
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := jlmem_target
+LOCAL_SRC_FILES := target_probe.cpp
+LOCAL_CPPFLAGS := -std=c++17 -Wall -Wextra -Werror
+LOCAL_CPP_FEATURES := exceptions
+LOCAL_LDLIBS := -llog
+include $(BUILD_SHARED_LIBRARY)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := jlmem
+LOCAL_SRC_FILES := memory_engine.cpp
+LOCAL_CPPFLAGS := -std=c++17 -Wall -Wextra -Werror
+LOCAL_CPP_FEATURES := exceptions
+LOCAL_LDLIBS := -llog
+include $(BUILD_SHARED_LIBRARY)
diff --git a/app/src/main/cpp/memory/memory_engine.cpp b/app/src/main/cpp/memory/memory_engine.cpp
new file mode 100644
index 000000000..605c0b2a8
--- /dev/null
+++ b/app/src/main/cpp/memory/memory_engine.cpp
@@ -0,0 +1,1204 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace {
+
+constexpr jint kOk = 0;
+constexpr jint kCancelled = 1;
+constexpr jint kInvalidRequest = 2;
+constexpr jint kResourceLimit = 3;
+constexpr jint kTargetLost = 5;
+constexpr jint kNoSession = 6;
+
+constexpr jint kTypeAuto = 0;
+constexpr jint kTypeByte = 1;
+constexpr jint kTypeShort = 2;
+constexpr jint kTypeChar = 3;
+constexpr jint kTypeInt = 4;
+constexpr jint kTypeLong = 5;
+constexpr jint kTypeFloat = 6;
+constexpr jint kTypeDouble = 7;
+
+constexpr jint kEqual = 0;
+constexpr jint kNotEqual = 1;
+constexpr jint kGreater = 2;
+constexpr jint kLess = 3;
+constexpr jint kGreaterOrEqual = 4;
+constexpr jint kLessOrEqual = 5;
+constexpr jint kBetween = 6;
+constexpr jint kChanged = 7;
+constexpr jint kUnchanged = 8;
+constexpr jint kIncreased = 9;
+constexpr jint kDecreased = 10;
+constexpr jint kIncreasedBy = 11;
+constexpr jint kDecreasedBy = 12;
+constexpr jint kChangedBy = 13;
+constexpr jint kIncreasedByRange = 14;
+constexpr jint kDecreasedByRange = 15;
+
+constexpr jint kComparePrevious = 0;
+constexpr jint kCompareInitial = 1;
+constexpr jint kStable = 0;
+// Candidate records are compact native data and never cross Binder in bulk. Two million typed
+// aliases covers the dense searches seen in the prototype while retaining a deterministic bound
+// on old API 23 devices. An incomplete set is never committed.
+constexpr size_t kCandidateLimit = 2'000'000;
+constexpr size_t kSnapshotByteLimit = 96U * 1024U * 1024U;
+constexpr size_t kReadChunkSize = 256U * 1024U;
+constexpr size_t kHistoryLimit = 8;
+constexpr size_t kHistoryByteLimit = 192U * 1024U * 1024U;
+constexpr size_t kResultStride = 7;
+
+struct Range {
+ uintptr_t start;
+ uintptr_t end;
+};
+
+struct Target {
+ pid_t pid = 0;
+ size_t pageSize = 0;
+ jlong token = 0;
+ uint64_t generation = 0;
+ std::vector ranges;
+};
+
+struct Candidate {
+ uint64_t id;
+ uintptr_t address;
+ jint type;
+ jint state;
+ uint64_t initialBits;
+ uint64_t previousBits;
+ uint64_t currentBits;
+};
+
+struct SnapshotRun {
+ uintptr_t start;
+ std::vector bytes;
+};
+
+enum class StateMode {
+ Empty,
+ Unknown,
+ Candidates,
+};
+
+struct SearchState {
+ StateMode mode = StateMode::Empty;
+ jint requestedType = kTypeAuto;
+ uint64_t logicalCount = 0;
+ std::vector snapshots;
+ std::vector candidates;
+
+ size_t retainedBytes() const {
+ size_t result =
+ sizeof(SearchState) + candidates.size() * sizeof(Candidate);
+ for (const SnapshotRun &snapshot : snapshots) {
+ if (result >
+ std::numeric_limits::max() - snapshot.bytes.size()) {
+ return std::numeric_limits::max();
+ }
+ result += snapshot.bytes.size();
+ }
+ return result;
+ }
+};
+
+struct Query {
+ jint type = 0;
+ bool floating = false;
+ int64_t integerFirst = 0;
+ int64_t integerSecond = 0;
+ uint64_t deltaFirst = 0;
+ uint64_t deltaSecond = 0;
+ double floatingFirst = 0;
+ double floatingSecond = 0;
+};
+
+std::mutex gMutex;
+Target gTarget;
+const std::shared_ptr gEmptyState =
+ std::make_shared();
+std::shared_ptr gState = gEmptyState;
+std::deque> gHistory;
+uint64_t gNextCandidateId = 1;
+std::atomic gCancelled{false};
+std::string gLastMessage;
+
+void setMessage(const char *message) {
+ std::lock_guard lock(gMutex);
+ gLastMessage = message;
+}
+
+size_t widthOf(jint type) {
+ switch (type) {
+ case kTypeByte:
+ return 1;
+ case kTypeShort:
+ case kTypeChar:
+ return 2;
+ case kTypeInt:
+ case kTypeFloat:
+ return 4;
+ case kTypeLong:
+ case kTypeDouble:
+ return 8;
+ default:
+ return 0;
+ }
+}
+
+std::vector expandedTypes(jint requestedType) {
+ if (requestedType == kTypeAuto) {
+ return {kTypeByte, kTypeShort, kTypeChar, kTypeInt,
+ kTypeLong, kTypeFloat, kTypeDouble};
+ }
+ if (widthOf(requestedType) == 0) {
+ return {};
+ }
+ return {requestedType};
+}
+
+bool parseInteger(const std::string &text, jint type, int64_t &value) {
+ if (text.empty()) {
+ return false;
+ }
+ errno = 0;
+ char *end = nullptr;
+ const char *number = text.c_str();
+ while (*number == ' ' || *number == '\t' || *number == '\r' ||
+ *number == '\n') {
+ ++number;
+ }
+ const char *prefix = *number == '+' || *number == '-' ? number + 1 : number;
+ const int base = prefix[0] == '0' && (prefix[1] == 'x' || prefix[1] == 'X')
+ ? 16
+ : 10;
+ const long long parsed = std::strtoll(text.c_str(), &end, base);
+ while (end != nullptr &&
+ (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) {
+ ++end;
+ }
+ if (errno == ERANGE || end == text.c_str() || end == nullptr ||
+ *end != '\0') {
+ return false;
+ }
+ int64_t minimum = std::numeric_limits::min();
+ int64_t maximum = std::numeric_limits::max();
+ switch (type) {
+ case kTypeByte:
+ minimum = std::numeric_limits::min();
+ maximum = std::numeric_limits::max();
+ break;
+ case kTypeShort:
+ minimum = std::numeric_limits::min();
+ maximum = std::numeric_limits::max();
+ break;
+ case kTypeChar:
+ minimum = 0;
+ maximum = std::numeric_limits::max();
+ break;
+ case kTypeInt:
+ minimum = std::numeric_limits::min();
+ maximum = std::numeric_limits::max();
+ break;
+ case kTypeLong:
+ break;
+ default:
+ return false;
+ }
+ value = static_cast(parsed);
+ return value >= minimum && value <= maximum;
+}
+
+bool parseFloating(const std::string &text, double &value) {
+ if (text.empty()) {
+ return false;
+ }
+ errno = 0;
+ char *end = nullptr;
+ value = std::strtod(text.c_str(), &end);
+ while (end != nullptr &&
+ (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) {
+ ++end;
+ }
+ return errno != ERANGE && end != text.c_str() && end != nullptr &&
+ *end == '\0' && std::isfinite(value);
+}
+
+bool parseDelta(const std::string &text, jint type, uint64_t &value) {
+ if (text.empty()) {
+ return false;
+ }
+ const char *number = text.c_str();
+ while (*number == ' ' || *number == '\t' || *number == '\r' ||
+ *number == '\n') {
+ ++number;
+ }
+ if (*number == '-') {
+ return false;
+ }
+ if (*number == '+') {
+ ++number;
+ }
+ const int base = number[0] == '0' && (number[1] == 'x' || number[1] == 'X')
+ ? 16
+ : 10;
+ errno = 0;
+ char *end = nullptr;
+ const unsigned long long parsed = std::strtoull(text.c_str(), &end, base);
+ while (end != nullptr &&
+ (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) {
+ ++end;
+ }
+ if (errno == ERANGE || end == text.c_str() || end == nullptr ||
+ *end != '\0') {
+ return false;
+ }
+ uint64_t maximum = 0;
+ switch (type) {
+ case kTypeByte:
+ maximum = std::numeric_limits::max();
+ break;
+ case kTypeShort:
+ case kTypeChar:
+ maximum = std::numeric_limits::max();
+ break;
+ case kTypeInt:
+ maximum = std::numeric_limits::max();
+ break;
+ case kTypeLong:
+ maximum = std::numeric_limits::max();
+ break;
+ default:
+ return false;
+ }
+ value = static_cast(parsed);
+ return value <= maximum;
+}
+
+bool predicateNeedsFirst(jint predicate) {
+ return predicate <= kBetween || predicate >= kIncreasedBy;
+}
+
+bool predicateNeedsSecond(jint predicate) {
+ return predicate == kBetween || predicate == kIncreasedByRange ||
+ predicate == kDecreasedByRange;
+}
+
+bool parseQuery(jint type, jint predicate, const std::string &first,
+ const std::string &second, Query &query) {
+ query.type = type;
+ query.floating = type == kTypeFloat || type == kTypeDouble;
+ const bool deltaPredicate = predicate >= kIncreasedBy;
+ if (predicateNeedsFirst(predicate)) {
+ if (query.floating) {
+ if (!parseFloating(first, query.floatingFirst)) {
+ return false;
+ }
+ if (deltaPredicate && query.floatingFirst < 0) {
+ return false;
+ }
+ if (type == kTypeFloat) {
+ const float rounded = static_cast(query.floatingFirst);
+ if (!std::isfinite(rounded)) {
+ return false;
+ }
+ query.floatingFirst = rounded;
+ }
+ } else if (deltaPredicate) {
+ if (!parseDelta(first, type, query.deltaFirst)) {
+ return false;
+ }
+ } else if (!parseInteger(first, type, query.integerFirst)) {
+ return false;
+ }
+ }
+ if (predicateNeedsSecond(predicate)) {
+ if (query.floating) {
+ if (!parseFloating(second, query.floatingSecond) ||
+ query.floatingFirst > query.floatingSecond) {
+ return false;
+ }
+ if (deltaPredicate && query.floatingSecond < 0) {
+ return false;
+ }
+ if (type == kTypeFloat) {
+ const float rounded = static_cast(query.floatingSecond);
+ if (!std::isfinite(rounded)) {
+ return false;
+ }
+ query.floatingSecond = rounded;
+ }
+ } else if (deltaPredicate) {
+ if (!parseDelta(second, type, query.deltaSecond) ||
+ query.deltaFirst > query.deltaSecond) {
+ return false;
+ }
+ } else if (!parseInteger(second, type, query.integerSecond) ||
+ query.integerFirst > query.integerSecond) {
+ return false;
+ }
+ }
+ return true;
+}
+
+uint64_t loadBits(const uint8_t *data, size_t width) {
+ uint64_t result = 0;
+ std::memcpy(&result, data, width);
+ return result;
+}
+
+int64_t integerValue(jint type, uint64_t bits) {
+ switch (type) {
+ case kTypeByte:
+ return static_cast(bits);
+ case kTypeShort:
+ return static_cast(bits);
+ case kTypeChar:
+ return static_cast(bits);
+ case kTypeInt:
+ return static_cast(bits);
+ case kTypeLong:
+ return static_cast(bits);
+ default:
+ return 0;
+ }
+}
+
+double floatingValue(jint type, uint64_t bits) {
+ if (type == kTypeFloat) {
+ uint32_t raw = static_cast(bits);
+ float value = 0;
+ std::memcpy(&value, &raw, sizeof(value));
+ return value;
+ }
+ double value = 0;
+ std::memcpy(&value, &bits, sizeof(value));
+ return value;
+}
+
+template
+bool matchesOrdered(T value, jint predicate, T first, T second) {
+ switch (predicate) {
+ case kEqual:
+ return value == first;
+ case kNotEqual:
+ return value != first;
+ case kGreater:
+ return value > first;
+ case kLess:
+ return value < first;
+ case kGreaterOrEqual:
+ return value >= first;
+ case kLessOrEqual:
+ return value <= first;
+ case kBetween:
+ return value >= first && value <= second;
+ default:
+ return false;
+ }
+}
+
+bool matchesKnown(uint64_t bits, const Query &query, jint predicate) {
+ if (query.floating) {
+ const double value = floatingValue(query.type, bits);
+ return !std::isnan(value) &&
+ matchesOrdered(value, predicate, query.floatingFirst,
+ query.floatingSecond);
+ }
+ return matchesOrdered(integerValue(query.type, bits), predicate,
+ query.integerFirst, query.integerSecond);
+}
+
+bool matchesRelative(uint64_t currentBits, uint64_t referenceBits,
+ const Query &query, jint predicate) {
+ if (query.floating) {
+ const double current = floatingValue(query.type, currentBits);
+ const double reference = floatingValue(query.type, referenceBits);
+ if (std::isnan(current) || std::isnan(reference)) {
+ return false;
+ }
+ const double delta = current - reference;
+ switch (predicate) {
+ case kChanged:
+ return current != reference;
+ case kUnchanged:
+ return current == reference;
+ case kIncreased:
+ return current > reference;
+ case kDecreased:
+ return current < reference;
+ case kIncreasedBy:
+ return delta == query.floatingFirst;
+ case kDecreasedBy:
+ return -delta == query.floatingFirst;
+ case kChangedBy:
+ return std::fabs(delta) == std::fabs(query.floatingFirst);
+ case kIncreasedByRange:
+ return delta >= query.floatingFirst &&
+ delta <= query.floatingSecond;
+ case kDecreasedByRange:
+ return -delta >= query.floatingFirst &&
+ -delta <= query.floatingSecond;
+ default:
+ return false;
+ }
+ }
+
+ const int64_t current = integerValue(query.type, currentBits);
+ const int64_t reference = integerValue(query.type, referenceBits);
+ const bool increased = current >= reference;
+ const uint64_t magnitude =
+ increased ? static_cast(current) -
+ static_cast(reference)
+ : static_cast(reference) -
+ static_cast(current);
+ switch (predicate) {
+ case kChanged:
+ return current != reference;
+ case kUnchanged:
+ return current == reference;
+ case kIncreased:
+ return current > reference;
+ case kDecreased:
+ return current < reference;
+ case kIncreasedBy:
+ return increased && magnitude == query.deltaFirst;
+ case kDecreasedBy:
+ return !increased && magnitude == query.deltaFirst;
+ case kChangedBy:
+ return magnitude == query.deltaFirst;
+ case kIncreasedByRange:
+ return increased && magnitude >= query.deltaFirst &&
+ magnitude <= query.deltaSecond;
+ case kDecreasedByRange:
+ return !increased && magnitude >= query.deltaFirst &&
+ magnitude <= query.deltaSecond;
+ default:
+ return false;
+ }
+}
+
+bool readExact(pid_t pid, uintptr_t address, void *destination, size_t size) {
+ auto *output = static_cast(destination);
+ size_t completed = 0;
+ while (completed < size) {
+ iovec local{output + completed, size - completed};
+ iovec remote{reinterpret_cast(address + completed),
+ size - completed};
+ const ssize_t read = process_vm_readv(pid, &local, 1, &remote, 1, 0);
+ if (read < 0 && errno == EINTR) {
+ continue;
+ }
+ if (read <= 0) {
+ return false;
+ }
+ completed += static_cast(read);
+ }
+ return true;
+}
+
+bool safeAdd(uint64_t &value, uint64_t addition) {
+ if (value > std::numeric_limits::max() - addition) {
+ value = std::numeric_limits::max();
+ return false;
+ }
+ value += addition;
+ return true;
+}
+
+struct OperationContext {
+ Target target;
+ std::shared_ptr state;
+ uint64_t nextId;
+};
+
+bool beginOperation(OperationContext &context) {
+ gCancelled.store(false, std::memory_order_release);
+ std::lock_guard lock(gMutex);
+ if (gTarget.pid <= 0 || gTarget.token == 0 || gTarget.ranges.empty()) {
+ gLastMessage = "No configured MIDlet runtime";
+ return false;
+ }
+ context.target = gTarget;
+ context.state = gState;
+ context.nextId = gNextCandidateId;
+ return true;
+}
+
+void trimHistoryLocked() {
+ size_t retained = 0;
+ for (const auto &state : gHistory) {
+ const size_t bytes = state->retainedBytes();
+ retained = retained > std::numeric_limits::max() - bytes
+ ? std::numeric_limits::max()
+ : retained + bytes;
+ }
+ while (!gHistory.empty() &&
+ (gHistory.size() > kHistoryLimit || retained > kHistoryByteLimit)) {
+ const size_t removed = gHistory.front()->retainedBytes();
+ gHistory.pop_front();
+ retained = removed > retained ? 0 : retained - removed;
+ }
+}
+
+jint commitOperation(const OperationContext &context,
+ std::shared_ptr next, bool preserveHistory) {
+ if (gCancelled.load(std::memory_order_acquire)) {
+ setMessage("Operation cancelled; previous results were preserved");
+ return kCancelled;
+ }
+ std::lock_guard lock(gMutex);
+ if (gTarget.generation != context.target.generation ||
+ gTarget.token != context.target.token) {
+ gLastMessage = "MIDlet runtime changed during the operation";
+ return kTargetLost;
+ }
+ if (preserveHistory && gState->mode != StateMode::Empty) {
+ gHistory.push_back(gState);
+ trimHistoryLocked();
+ } else if (!preserveHistory) {
+ gHistory.clear();
+ }
+ gState = std::move(next);
+ gNextCandidateId = context.nextId;
+ gLastMessage = "";
+ return kOk;
+}
+
+bool buildQueries(jint requestedType, jint predicate, const std::string &first,
+ const std::string &second, bool relative,
+ std::vector &queries) {
+ if ((!relative && (predicate < kEqual || predicate > kBetween)) ||
+ (relative && (predicate < kChanged || predicate > kDecreasedByRange))) {
+ return false;
+ }
+ for (jint type : expandedTypes(requestedType)) {
+ Query query;
+ if (parseQuery(type, predicate, first, second, query)) {
+ queries.push_back(query);
+ } else if (requestedType != kTypeAuto) {
+ return false;
+ }
+ }
+ return !queries.empty();
+}
+
+jint scanKnown(const OperationContext &context, jint requestedType,
+ jint predicate, const std::string &first,
+ const std::string &second) {
+ if (context.nextId >
+ std::numeric_limits::max() - kCandidateLimit) {
+ setMessage("Candidate identifier space is exhausted for this runtime");
+ return kResourceLimit;
+ }
+ std::vector queries;
+ if (!buildQueries(requestedType, predicate, first, second, false,
+ queries)) {
+ setMessage("Invalid value, type, or predicate");
+ return kInvalidRequest;
+ }
+ auto next = std::make_shared();
+ next->mode = StateMode::Candidates;
+ next->requestedType = requestedType;
+ std::vector buffer;
+
+ for (const Range &range : context.target.ranges) {
+ for (uintptr_t chunkStart = range.start; chunkStart < range.end;) {
+ if (gCancelled.load(std::memory_order_acquire)) {
+ setMessage(
+ "Operation cancelled; previous results were preserved");
+ return kCancelled;
+ }
+ const size_t remaining =
+ static_cast(range.end - chunkStart);
+ const size_t chunkSize = std::min(remaining, kReadChunkSize);
+ buffer.resize(chunkSize);
+ if (!readExact(context.target.pid, chunkStart, buffer.data(),
+ chunkSize)) {
+ setMessage("A target range changed while it was being scanned");
+ return kTargetLost;
+ }
+ for (const Query &query : queries) {
+ const size_t width = widthOf(query.type);
+ uintptr_t address = chunkStart;
+ const size_t misalignment =
+ static_cast(address % width);
+ if (misalignment != 0) {
+ address += width - misalignment;
+ }
+ while (address >= chunkStart &&
+ address <= chunkStart + chunkSize -
+ std::min(width, chunkSize)) {
+ const size_t offset =
+ static_cast(address - chunkStart);
+ if (offset + width > chunkSize) {
+ break;
+ }
+ const uint64_t bits =
+ loadBits(buffer.data() + offset, width);
+ if (matchesKnown(bits, query, predicate)) {
+ if (next->candidates.size() >= kCandidateLimit) {
+ setMessage("Candidate limit reached; previous "
+ "results were preserved");
+ return kResourceLimit;
+ }
+ next->candidates.push_back(
+ {context.nextId + next->candidates.size(),
+ address, query.type, kStable, bits, bits,
+ bits});
+ }
+ if (address >
+ std::numeric_limits::max() - width) {
+ break;
+ }
+ address += width;
+ }
+ }
+ chunkStart += chunkSize;
+ }
+ }
+ next->logicalCount = next->candidates.size();
+ OperationContext committed = context;
+ committed.nextId += next->candidates.size();
+ return commitOperation(committed, std::move(next), false);
+}
+
+jint snapshotUnknown(const OperationContext &context, jint requestedType) {
+ const std::vector types = expandedTypes(requestedType);
+ if (types.empty()) {
+ setMessage("Invalid value type");
+ return kInvalidRequest;
+ }
+ auto next = std::make_shared();
+ next->mode = StateMode::Unknown;
+ next->requestedType = requestedType;
+ size_t retained = 0;
+ for (const Range &range : context.target.ranges) {
+ if (gCancelled.load(std::memory_order_acquire)) {
+ setMessage("Operation cancelled; previous results were preserved");
+ return kCancelled;
+ }
+ const size_t size = static_cast(range.end - range.start);
+ if (size >
+ kSnapshotByteLimit - std::min(retained, kSnapshotByteLimit)) {
+ setMessage("Unknown-value snapshot exceeds the memory budget");
+ return kResourceLimit;
+ }
+ SnapshotRun snapshot;
+ snapshot.start = range.start;
+ snapshot.bytes.resize(size);
+ if (!readExact(context.target.pid, range.start, snapshot.bytes.data(),
+ size)) {
+ setMessage("A target range changed while it was being captured");
+ return kTargetLost;
+ }
+ retained += size;
+ for (jint type : types) {
+ const size_t width = widthOf(type);
+ const size_t adjustment =
+ range.start % width == 0 ? 0 : width - range.start % width;
+ if (range.start >
+ std::numeric_limits::max() - adjustment) {
+ continue;
+ }
+ const uintptr_t aligned = range.start + adjustment;
+ if (aligned < range.end &&
+ static_cast(range.end - aligned) >= width) {
+ safeAdd(next->logicalCount,
+ 1U + static_cast(
+ (range.end - aligned - width) / width));
+ }
+ }
+ next->snapshots.push_back(std::move(snapshot));
+ }
+ return commitOperation(context, std::move(next), false);
+}
+
+jint captureCurrentImage(const Target &target,
+ std::vector &snapshots) {
+ size_t retained = 0;
+ snapshots.clear();
+ snapshots.reserve(target.ranges.size());
+ for (const Range &range : target.ranges) {
+ if (gCancelled.load(std::memory_order_acquire)) {
+ setMessage("Operation cancelled; previous results were preserved");
+ return kCancelled;
+ }
+ const size_t size = static_cast(range.end - range.start);
+ if (size > kSnapshotByteLimit - std::min(retained, kSnapshotByteLimit)) {
+ setMessage("Refine snapshot exceeds the memory budget");
+ return kResourceLimit;
+ }
+ SnapshotRun snapshot;
+ snapshot.start = range.start;
+ snapshot.bytes.resize(size);
+ if (!readExact(target.pid, range.start, snapshot.bytes.data(), size)) {
+ setMessage("A target range changed while it was being refined");
+ return kTargetLost;
+ }
+ retained += size;
+ snapshots.push_back(std::move(snapshot));
+ }
+ return kOk;
+}
+
+bool readImageBits(const std::vector &snapshots,
+ const Candidate &candidate, uint64_t &bits) {
+ const size_t width = widthOf(candidate.type);
+ if (width == 0) {
+ return false;
+ }
+ const auto run = std::upper_bound(
+ snapshots.begin(), snapshots.end(), candidate.address,
+ [](uintptr_t address, const SnapshotRun &item) {
+ return address < item.start;
+ });
+ if (run == snapshots.begin()) {
+ return false;
+ }
+ const SnapshotRun &snapshot = *std::prev(run);
+ if (candidate.address < snapshot.start) {
+ return false;
+ }
+ const size_t offset = static_cast(candidate.address - snapshot.start);
+ if (offset > snapshot.bytes.size() ||
+ width > snapshot.bytes.size() - offset) {
+ return false;
+ }
+ bits = loadBits(snapshot.bytes.data() + offset, width);
+ return true;
+}
+
+jint refineCandidates(const OperationContext &context, jint predicate,
+ const std::string &first, const std::string &second,
+ bool relative, jint compareTarget) {
+ if (context.state->mode == StateMode::Empty) {
+ setMessage("No search session to refine");
+ return kNoSession;
+ }
+ if (context.state->mode == StateMode::Unknown &&
+ context.nextId >
+ std::numeric_limits::max() - kCandidateLimit) {
+ setMessage("Candidate identifier space is exhausted for this runtime");
+ return kResourceLimit;
+ }
+ if (relative && compareTarget != kComparePrevious &&
+ compareTarget != kCompareInitial) {
+ setMessage("Invalid comparison target");
+ return kInvalidRequest;
+ }
+ std::vector queries;
+ if (!buildQueries(context.state->requestedType, predicate, first, second,
+ relative, queries)) {
+ setMessage("Invalid value, type, or predicate");
+ return kInvalidRequest;
+ }
+ auto next = std::make_shared();
+ next->mode = StateMode::Candidates;
+ next->requestedType = context.state->requestedType;
+
+ if (context.state->mode == StateMode::Unknown) {
+ for (const SnapshotRun &snapshot : context.state->snapshots) {
+ std::vector current(snapshot.bytes.size());
+ if (gCancelled.load(std::memory_order_acquire)) {
+ setMessage(
+ "Operation cancelled; previous results were preserved");
+ return kCancelled;
+ }
+ if (!readExact(context.target.pid, snapshot.start, current.data(),
+ current.size())) {
+ setMessage("A target range changed while it was being refined");
+ return kTargetLost;
+ }
+ for (const Query &query : queries) {
+ const size_t width = widthOf(query.type);
+ uintptr_t address = snapshot.start;
+ const size_t misalignment =
+ static_cast(address % width);
+ if (misalignment != 0) {
+ address += width - misalignment;
+ }
+ while (address >= snapshot.start) {
+ const size_t offset =
+ static_cast(address - snapshot.start);
+ if (offset + width > snapshot.bytes.size()) {
+ break;
+ }
+ const uint64_t initial =
+ loadBits(snapshot.bytes.data() + offset, width);
+ const uint64_t now =
+ loadBits(current.data() + offset, width);
+ const bool match =
+ relative ? matchesRelative(now, initial, query,
+ predicate)
+ : matchesKnown(now, query, predicate);
+ if (match) {
+ if (next->candidates.size() >= kCandidateLimit) {
+ setMessage("Candidate limit reached; previous "
+ "results were preserved");
+ return kResourceLimit;
+ }
+ next->candidates.push_back(
+ {context.nextId + next->candidates.size(),
+ address, query.type, kStable, initial, initial,
+ now});
+ }
+ if (address >
+ std::numeric_limits::max() - width) {
+ break;
+ }
+ address += width;
+ }
+ }
+ }
+ } else {
+ // Reading one complete resident image turns a million-candidate refine from a million
+ // process_vm_readv syscalls into sequential remote reads plus an in-process filter pass.
+ // It also makes a published zero trustworthy: partial coverage aborts transactionally.
+ std::vector currentImage;
+ const jint captureResult =
+ captureCurrentImage(context.target, currentImage);
+ if (captureResult != kOk) {
+ return captureResult;
+ }
+ next->candidates.reserve(context.state->candidates.size());
+ for (const Candidate &candidate : context.state->candidates) {
+ if (gCancelled.load(std::memory_order_acquire)) {
+ setMessage(
+ "Operation cancelled; previous results were preserved");
+ return kCancelled;
+ }
+ Candidate updated = candidate;
+ uint64_t current = 0;
+ if (!readImageBits(currentImage, candidate, current)) {
+ // The complete image was captured successfully, so an unresolved address is a
+ // stale binding. Never keep it as a result that looks editable.
+ continue;
+ }
+ updated.previousBits = candidate.currentBits;
+ updated.currentBits = current;
+ updated.state = kStable;
+ auto query = std::find_if(queries.begin(), queries.end(),
+ [&](const Query &item) {
+ return item.type == candidate.type;
+ });
+ if (query == queries.end()) {
+ continue;
+ }
+ const uint64_t reference = compareTarget == kCompareInitial
+ ? candidate.initialBits
+ : candidate.currentBits;
+ const bool match =
+ relative ? matchesRelative(current, reference, *query,
+ predicate)
+ : matchesKnown(current, *query, predicate);
+ if (match) {
+ next->candidates.push_back(updated);
+ }
+ }
+ }
+ next->logicalCount = next->candidates.size();
+ OperationContext committed = context;
+ if (context.state->mode == StateMode::Unknown) {
+ committed.nextId += next->candidates.size();
+ }
+ return commitOperation(committed, std::move(next), true);
+}
+
+std::string fromJString(JNIEnv *env, jstring value) {
+ if (value == nullptr) {
+ return {};
+ }
+ const char *characters = env->GetStringUTFChars(value, nullptr);
+ if (characters == nullptr) {
+ return {};
+ }
+ std::string result(characters);
+ env->ReleaseStringUTFChars(value, characters);
+ return result;
+}
+
+template jint guardedOperation(Operation operation) {
+ try {
+ return operation();
+ } catch (const std::bad_alloc &) {
+ setMessage("Memory budget could not be reserved; previous results were "
+ "preserved");
+ return kResourceLimit;
+ } catch (...) {
+ setMessage("The native engine rejected the operation safely");
+ return kInvalidRequest;
+ }
+}
+
+} // namespace
+
+extern "C" JNIEXPORT jint JNICALL
+Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_configureTarget(
+ JNIEnv *env, jclass, jint pid, jint pageSize, jlong token,
+ jlongArray rawRuns) {
+ try {
+ if (pid <= 0 || pageSize <= 0 || (pageSize & (pageSize - 1)) != 0 ||
+ token == 0 || rawRuns == nullptr) {
+ setMessage("Invalid target configuration");
+ return kInvalidRequest;
+ }
+ const jsize length = env->GetArrayLength(rawRuns);
+ if (length < 4 || (length - 2) % 2 != 0) {
+ setMessage("Invalid target range list");
+ return kInvalidRequest;
+ }
+ std::vector values(static_cast(length));
+ env->GetLongArrayRegion(rawRuns, 0, length, values.data());
+ if (env->ExceptionCheck() || values[1] != 0 ||
+ values[0] != (length - 2) / 2) {
+ setMessage("Incomplete target range list");
+ return kResourceLimit;
+ }
+
+ Target target;
+ target.pid = pid;
+ target.pageSize = static_cast(pageSize);
+ target.token = token;
+ uintptr_t previousEnd = 0;
+ for (jsize index = 2; index < length; index += 2) {
+ if (values[index] <= 0 || values[index + 1] <= values[index] ||
+ static_cast(values[index]) >
+ std::numeric_limits::max() ||
+ static_cast(values[index + 1]) >
+ std::numeric_limits::max()) {
+ setMessage("Invalid target range bounds");
+ return kInvalidRequest;
+ }
+ const uintptr_t start = static_cast(values[index]);
+ const uintptr_t end = static_cast(values[index + 1]);
+ if (start % target.pageSize != 0 || end % target.pageSize != 0 ||
+ (!target.ranges.empty() && start < previousEnd)) {
+ setMessage("Target ranges are unaligned or overlap");
+ return kInvalidRequest;
+ }
+ target.ranges.push_back({start, end});
+ previousEnd = end;
+ }
+
+ std::lock_guard lock(gMutex);
+ target.generation = gTarget.generation + 1;
+ const bool sameRuntime =
+ gTarget.pid == target.pid && gTarget.token == target.token;
+ gTarget = std::move(target);
+ if (!sameRuntime) {
+ gState = gEmptyState;
+ gHistory.clear();
+ gNextCandidateId = 1;
+ }
+ gCancelled.store(false, std::memory_order_release);
+ gLastMessage = "";
+ return kOk;
+ } catch (const std::bad_alloc &) {
+ setMessage("Target configuration exceeds the engine memory budget");
+ return kResourceLimit;
+ } catch (...) {
+ setMessage("Invalid target configuration");
+ return kInvalidRequest;
+ }
+}
+
+extern "C" JNIEXPORT jboolean JNICALL
+Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_canReadTarget(
+ JNIEnv *, jclass, jint pid, jlong address, jlong expectedBits) {
+ if (pid <= 0 || address <= 0 ||
+ static_cast(address) >
+ std::numeric_limits::max()) {
+ return JNI_FALSE;
+ }
+ uint64_t actual = 0;
+ return readExact(pid, static_cast(address), &actual,
+ sizeof(actual)) &&
+ actual == static_cast(expectedBits)
+ ? JNI_TRUE
+ : JNI_FALSE;
+}
+
+extern "C" JNIEXPORT jint JNICALL
+Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_startKnown(
+ JNIEnv *env, jclass, jint type, jint predicate, jstring first,
+ jstring second) {
+ return guardedOperation([&] {
+ OperationContext context;
+ if (!beginOperation(context)) {
+ return kNoSession;
+ }
+ return scanKnown(context, type, predicate, fromJString(env, first),
+ fromJString(env, second));
+ });
+}
+
+extern "C" JNIEXPORT jint JNICALL
+Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_startUnknown(
+ JNIEnv *, jclass, jint type) {
+ return guardedOperation([&] {
+ OperationContext context;
+ if (!beginOperation(context)) {
+ return kNoSession;
+ }
+ return snapshotUnknown(context, type);
+ });
+}
+
+extern "C" JNIEXPORT jint JNICALL
+Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_refineKnown(
+ JNIEnv *env, jclass, jint predicate, jstring first, jstring second) {
+ return guardedOperation([&] {
+ OperationContext context;
+ if (!beginOperation(context)) {
+ return kNoSession;
+ }
+ return refineCandidates(context, predicate, fromJString(env, first),
+ fromJString(env, second), false,
+ kComparePrevious);
+ });
+}
+
+extern "C" JNIEXPORT jint JNICALL
+Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_refineRelative(
+ JNIEnv *env, jclass, jint predicate, jint compareTarget, jstring first,
+ jstring second) {
+ return guardedOperation([&] {
+ OperationContext context;
+ if (!beginOperation(context)) {
+ return kNoSession;
+ }
+ return refineCandidates(context, predicate, fromJString(env, first),
+ fromJString(env, second), true, compareTarget);
+ });
+}
+
+extern "C" JNIEXPORT jint JNICALL
+Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_undo(JNIEnv *,
+ jclass) {
+ std::lock_guard lock(gMutex);
+ if (gHistory.empty()) {
+ gLastMessage = "No earlier search state is available";
+ return kNoSession;
+ }
+ gState = gHistory.back();
+ gHistory.pop_back();
+ gLastMessage = "";
+ return kOk;
+}
+
+extern "C" JNIEXPORT jlong JNICALL
+Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_resultCount(JNIEnv *,
+ jclass) {
+ std::shared_ptr state;
+ {
+ std::lock_guard lock(gMutex);
+ state = gState;
+ }
+ return state->logicalCount > static_cast(
+ std::numeric_limits::max())
+ ? std::numeric_limits::max()
+ : static_cast(state->logicalCount);
+}
+
+extern "C" JNIEXPORT jlongArray JNICALL
+Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_resultPage(
+ JNIEnv *env, jclass, jint offset, jint limit) {
+ if (offset < 0 || limit <= 0 || limit > 200) {
+ return nullptr;
+ }
+ std::shared_ptr state;
+ {
+ std::lock_guard lock(gMutex);
+ state = gState;
+ }
+ const size_t start =
+ std::min(static_cast(offset), state->candidates.size());
+ const size_t count = std::min(static_cast(limit),
+ state->candidates.size() - start);
+ const size_t outputSize = 1U + count * kResultStride;
+ std::vector output(outputSize);
+ output[0] = static_cast(count);
+ for (size_t index = 0; index < count; ++index) {
+ const Candidate &candidate = state->candidates[start + index];
+ const size_t base = 1U + index * kResultStride;
+ output[base] = static_cast(candidate.id);
+ output[base + 1U] = static_cast(candidate.address);
+ output[base + 2U] = candidate.type;
+ output[base + 3U] = candidate.state;
+ output[base + 4U] = static_cast(candidate.initialBits);
+ output[base + 5U] = static_cast(candidate.previousBits);
+ output[base + 6U] = static_cast(candidate.currentBits);
+ }
+ jlongArray result = env->NewLongArray(static_cast(outputSize));
+ if (result != nullptr) {
+ env->SetLongArrayRegion(result, 0, static_cast(outputSize),
+ output.data());
+ }
+ return result;
+}
+
+extern "C" JNIEXPORT void JNICALL
+Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_clear(JNIEnv *,
+ jclass) {
+ std::lock_guard lock(gMutex);
+ ++gTarget.generation;
+ gTarget.pid = 0;
+ gTarget.pageSize = 0;
+ gTarget.token = 0;
+ gTarget.ranges.clear();
+ gState = gEmptyState;
+ gHistory.clear();
+ gNextCandidateId = 1;
+ gLastMessage = "";
+}
+
+extern "C" JNIEXPORT void JNICALL
+Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_cancel(JNIEnv *,
+ jclass) {
+ gCancelled.store(true, std::memory_order_release);
+}
+
+extern "C" JNIEXPORT jstring JNICALL
+Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_lastMessage(
+ JNIEnv *env, jclass) {
+ std::string message;
+ {
+ std::lock_guard lock(gMutex);
+ message = gLastMessage;
+ }
+ return env->NewStringUTF(message.c_str());
+}
diff --git a/app/src/main/cpp/memory/target_probe.cpp b/app/src/main/cpp/memory/target_probe.cpp
new file mode 100644
index 000000000..d9a5d853d
--- /dev/null
+++ b/app/src/main/cpp/memory/target_probe.cpp
@@ -0,0 +1,223 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace {
+
+constexpr jint kFastScope = 0;
+constexpr jint kThoroughScope = 1;
+alignas(uint64_t) volatile uint64_t gReadProbe = UINT64_C(0x4a4c4d454d50524f);
+
+bool isSelectedMap(const char *permissions, const std::string &name,
+ jint scope) {
+ if (std::strncmp(permissions, "rw-p", 4) != 0) {
+ return false;
+ }
+
+ const bool dalvik = name.find("dalvik-") != std::string::npos;
+ const bool zygote = name.find("zygote") != std::string::npos;
+ if (scope == kFastScope) {
+ return dalvik && !zygote;
+ }
+ if (scope != kThoroughScope) {
+ return false;
+ }
+
+ // ART names its managed-heap mappings on current releases. Also accept
+ // unnamed private anonymous mappings so the thorough scope remains useful
+ // on runtimes that omit those labels. File-backed and explicitly named
+ // non-ART mappings stay out.
+ return (dalvik && !zygote) || name.empty();
+}
+
+bool appendRun(std::vector> &runs,
+ uintptr_t start, uintptr_t end, size_t maxRuns,
+ bool &truncated) {
+ if (start >= end) {
+ return true;
+ }
+ if (!runs.empty() && runs.back().second == start) {
+ runs.back().second = end;
+ return true;
+ }
+ if (runs.size() >= maxRuns) {
+ truncated = true;
+ return false;
+ }
+ runs.emplace_back(start, end);
+ return true;
+}
+
+} // namespace
+
+extern "C" JNIEXPORT jint JNICALL
+Java_ru_playsoftware_j2meloader_memory_NativeMemoryTarget_pageSize(JNIEnv *,
+ jclass) {
+ const long value = sysconf(_SC_PAGESIZE);
+ if (value <= 0 || value > std::numeric_limits::max()) {
+ return 0;
+ }
+ return static_cast(value);
+}
+
+extern "C" JNIEXPORT jlongArray JNICALL
+Java_ru_playsoftware_j2meloader_memory_NativeMemoryTarget_readProbe(JNIEnv *env,
+ jclass) {
+ const jlong values[] = {
+ static_cast(reinterpret_cast(&gReadProbe)),
+ static_cast(gReadProbe),
+ };
+ jlongArray result = env->NewLongArray(2);
+ if (result != nullptr) {
+ env->SetLongArrayRegion(result, 0, 2, values);
+ }
+ return result;
+}
+
+extern "C" JNIEXPORT jlongArray JNICALL
+Java_ru_playsoftware_j2meloader_memory_NativeMemoryTarget_collectResidentRuns(
+ JNIEnv *env, jclass, jint scope, jint maxRuns) {
+ const long pageValue = sysconf(_SC_PAGESIZE);
+ if ((scope != kFastScope && scope != kThoroughScope) || maxRuns <= 0 ||
+ pageValue <= 0) {
+ return nullptr;
+ }
+ const size_t pageSize = static_cast(pageValue);
+
+ FILE *maps = nullptr;
+ char *line = nullptr;
+ try {
+ maps = std::fopen("/proc/self/maps", "re");
+ if (maps == nullptr) {
+ return nullptr;
+ }
+
+ std::vector> runs;
+ bool truncated = false;
+ size_t lineCapacity = 0;
+ while (!truncated && getline(&line, &lineCapacity, maps) >= 0) {
+ unsigned long long rawStart = 0;
+ unsigned long long rawEnd = 0;
+ char permissions[5] = {};
+ int nameOffset = 0;
+ if (std::sscanf(line, "%llx-%llx %4s %*s %*s %*s %n", &rawStart,
+ &rawEnd, permissions, &nameOffset) < 3 ||
+ rawStart >= rawEnd ||
+ rawEnd > std::numeric_limits::max()) {
+ continue;
+ }
+ std::string name;
+ if (nameOffset > 0) {
+ const char *begin = line + nameOffset;
+ while (*begin == ' ' || *begin == '\t') {
+ ++begin;
+ }
+ name.assign(begin);
+ while (!name.empty() &&
+ (name.back() == '\n' || name.back() == '\r' ||
+ name.back() == ' ' || name.back() == '\t')) {
+ name.pop_back();
+ }
+ }
+ if (!isSelectedMap(permissions, name, scope)) {
+ continue;
+ }
+
+ const uintptr_t start = static_cast(rawStart);
+ const uintptr_t end = static_cast(rawEnd);
+ if (start % pageSize != 0 || end % pageSize != 0) {
+ continue;
+ }
+ const size_t pageCount =
+ static_cast((end - start) / pageSize);
+ if (pageCount == 0) {
+ continue;
+ }
+ std::vector residency;
+ try {
+ residency.resize(pageCount);
+ } catch (...) {
+ truncated = true;
+ break;
+ }
+ if (mincore(reinterpret_cast(start), end - start,
+ residency.data()) != 0) {
+ // A map may disappear between maps parsing and mincore. It is
+ // safer to report an incomplete snapshot than to publish
+ // silently partial ranges.
+ truncated = true;
+ break;
+ }
+
+ size_t runStartPage = pageCount;
+ for (size_t page = 0; page <= pageCount; ++page) {
+ const bool resident =
+ page < pageCount && (residency[page] & 1U) != 0;
+ if (resident && runStartPage == pageCount) {
+ runStartPage = page;
+ } else if (!resident && runStartPage != pageCount) {
+ const uintptr_t runStart = start + runStartPage * pageSize;
+ const uintptr_t runEnd = start + page * pageSize;
+ if (!appendRun(runs, runStart, runEnd,
+ static_cast(maxRuns), truncated)) {
+ break;
+ }
+ runStartPage = pageCount;
+ }
+ }
+ }
+ std::free(line);
+ line = nullptr;
+ std::fclose(maps);
+ maps = nullptr;
+
+ if (runs.size() >
+ (static_cast(std::numeric_limits::max()) - 2U) /
+ 2U) {
+ return nullptr;
+ }
+ const jsize outputSize = static_cast(2U + runs.size() * 2U);
+ std::vector output(static_cast(outputSize));
+ output[0] = static_cast(runs.size());
+ output[1] = truncated ? 1 : 0;
+ for (size_t index = 0; index < runs.size(); ++index) {
+ output[2U + index * 2U] = static_cast(runs[index].first);
+ output[3U + index * 2U] = static_cast(runs[index].second);
+ }
+ jlongArray result = env->NewLongArray(outputSize);
+ if (result != nullptr) {
+ env->SetLongArrayRegion(result, 0, outputSize, output.data());
+ }
+ return result;
+ } catch (...) {
+ std::free(line);
+ if (maps != nullptr) {
+ std::fclose(maps);
+ }
+ return nullptr;
+ }
+}
diff --git a/app/src/main/java/javax/microedition/shell/MicroLoader.java b/app/src/main/java/javax/microedition/shell/MicroLoader.java
index f4bbc67d7..fcd9ebd3e 100644
--- a/app/src/main/java/javax/microedition/shell/MicroLoader.java
+++ b/app/src/main/java/javax/microedition/shell/MicroLoader.java
@@ -75,6 +75,7 @@
import ru.playsoftware.j2meloader.crashes.CrashReporter;
import ru.playsoftware.j2meloader.crashes.MidletSessionJournal;
import ru.playsoftware.j2meloader.crashes.MidletSessionStore;
+import ru.playsoftware.j2meloader.memory.MemoryRuntimeSession;
import ru.playsoftware.j2meloader.util.AppUtils;
import ru.playsoftware.j2meloader.util.FileUtils;
import ru.playsoftware.j2meloader.util.IOUtils;
@@ -103,6 +104,7 @@ public class MicroLoader {
private String jarSha256;
private TimingSession timingSession;
private AutoSpeedController autoSpeedController;
+ private long memoryRuntimeToken;
private boolean timingTransformCompatible;
/** Set only after the MIDlet thread has successfully received the timing session. */
private boolean timingSessionTransferred;
@@ -167,6 +169,7 @@ private void startTimingSession() {
}
timingSession = session;
autoSpeedController = speedController;
+ memoryRuntimeToken = MemoryRuntimeSession.start();
}
void closeTimingSession() {
@@ -174,6 +177,8 @@ void closeTimingSession() {
timingSession = null;
autoSpeedController = null;
GuestTimingBridge.clear(session);
+ MemoryRuntimeSession.close(memoryRuntimeToken);
+ memoryRuntimeToken = 0L;
}
/**
diff --git a/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryEngineContract.java b/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryEngineContract.java
new file mode 100644
index 000000000..216b35378
--- /dev/null
+++ b/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryEngineContract.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ru.playsoftware.j2meloader.memory;
+
+/** Stable primitive constants shared by the UI-independent engine IPC and native core. */
+public final class MemoryEngineContract {
+ public static final int SCOPE_JAVA_FAST = 0;
+ public static final int SCOPE_JAVA_THOROUGH = 1;
+
+ public static final int TYPE_AUTO = 0;
+ public static final int TYPE_BYTE = 1;
+ public static final int TYPE_SHORT = 2;
+ public static final int TYPE_CHAR = 3;
+ public static final int TYPE_INT = 4;
+ public static final int TYPE_LONG = 5;
+ public static final int TYPE_FLOAT = 6;
+ public static final int TYPE_DOUBLE = 7;
+
+ public static final int PREDICATE_EQUAL = 0;
+ public static final int PREDICATE_NOT_EQUAL = 1;
+ public static final int PREDICATE_GREATER = 2;
+ public static final int PREDICATE_LESS = 3;
+ public static final int PREDICATE_GREATER_OR_EQUAL = 4;
+ public static final int PREDICATE_LESS_OR_EQUAL = 5;
+ public static final int PREDICATE_BETWEEN = 6;
+ public static final int PREDICATE_CHANGED = 7;
+ public static final int PREDICATE_UNCHANGED = 8;
+ public static final int PREDICATE_INCREASED = 9;
+ public static final int PREDICATE_DECREASED = 10;
+ public static final int PREDICATE_INCREASED_BY = 11;
+ public static final int PREDICATE_DECREASED_BY = 12;
+ public static final int PREDICATE_CHANGED_BY = 13;
+ public static final int PREDICATE_INCREASED_BY_RANGE = 14;
+ public static final int PREDICATE_DECREASED_BY_RANGE = 15;
+
+ public static final int COMPARE_PREVIOUS = 0;
+ public static final int COMPARE_INITIAL = 1;
+
+ public static final int RESULT_OK = 0;
+ public static final int RESULT_CANCELLED = 1;
+ public static final int RESULT_INVALID_REQUEST = 2;
+ public static final int RESULT_RESOURCE_LIMIT = 3;
+ public static final int RESULT_UNSUPPORTED = 4;
+ public static final int RESULT_TARGET_LOST = 5;
+ public static final int RESULT_NO_SESSION = 6;
+
+ public static final int CANDIDATE_STABLE = 0;
+ public static final int CANDIDATE_RELOCATING = 1;
+ public static final int CANDIDATE_AMBIGUOUS = 2;
+ public static final int CANDIDATE_LOST = 3;
+
+ /** [count, id, address, type, state, initialBits, previousBits, currentBits, ...]. */
+ public static final int RESULT_PAGE_STRIDE = 7;
+ public static final int MAX_RESULT_PAGE_SIZE = 200;
+
+ public static final String KEY_SUPPORTED = "supported";
+ public static final String KEY_RUNTIME_TOKEN = "runtimeToken";
+ public static final String KEY_TARGET_PID = "targetPid";
+ public static final String KEY_PAGE_SIZE = "pageSize";
+ public static final String KEY_MESSAGE = "message";
+
+ private MemoryEngineContract() {
+ }
+
+ public static boolean isScope(int scope) {
+ return scope == SCOPE_JAVA_FAST || scope == SCOPE_JAVA_THOROUGH;
+ }
+
+ public static boolean isValueType(int type) {
+ return type >= TYPE_AUTO && type <= TYPE_DOUBLE;
+ }
+
+ public static boolean isCandidateType(int type) {
+ return type >= TYPE_BYTE && type <= TYPE_DOUBLE;
+ }
+
+ static boolean isCompleteRunList(long[] runs) {
+ if (runs == null || runs.length < 2 || runs[0] <= 0L || runs[1] != 0L ||
+ runs[0] > (runs.length - 2L) / 2L) {
+ return false;
+ }
+ return runs.length == 2 + (int) runs[0] * 2;
+ }
+}
diff --git a/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryEngineService.java b/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryEngineService.java
new file mode 100644
index 000000000..a9844faa7
--- /dev/null
+++ b/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryEngineService.java
@@ -0,0 +1,363 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ru.playsoftware.j2meloader.memory;
+
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+
+import androidx.annotation.Nullable;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.atomic.AtomicLong;
+
+/** Owns all scan state in a dedicated app process and exposes only logical candidate IDs. */
+public final class MemoryEngineService extends Service {
+ private static final int MAX_RUNS = 4096;
+
+ private final AtomicLong nextOperationId = new AtomicLong(1L);
+ private final AtomicLong cancelEpoch = new AtomicLong();
+ private final RemoteCallbackList callbacks = new RemoteCallbackList<>();
+ private final ExecutorService worker = Executors.newSingleThreadExecutor(runnable -> {
+ Thread thread = new Thread(runnable, "MemoryEditorEngine");
+ thread.setPriority(Thread.NORM_PRIORITY - 1);
+ return thread;
+ });
+ private volatile IMemoryTargetBridge target;
+ private volatile boolean targetBound;
+ private volatile long configuredToken;
+ private final IMemoryTargetCallback targetCallback = new IMemoryTargetCallback.Stub() {
+ @Override
+ public void onRuntimeEnded(long runtimeToken) {
+ if (runtimeToken == configuredToken) {
+ invalidateTarget();
+ }
+ }
+ };
+
+ private final ServiceConnection targetConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ IMemoryTargetBridge bridge = IMemoryTargetBridge.Stub.asInterface(service);
+ target = bridge;
+ try {
+ bridge.registerTargetCallback(targetCallback);
+ } catch (RemoteException exception) {
+ target = null;
+ invalidateTarget();
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ target = null;
+ invalidateTarget();
+ }
+
+ @Override
+ public void onBindingDied(ComponentName name) {
+ target = null;
+ invalidateTarget();
+ }
+ };
+
+ private final IMemoryEngineService.Stub binder = new IMemoryEngineService.Stub() {
+ @Override
+ public Bundle getCapabilities() {
+ Bundle result = new Bundle();
+ IMemoryTargetBridge bridge = target;
+ if (bridge == null) {
+ result.putBoolean(MemoryEngineContract.KEY_SUPPORTED, false);
+ result.putString(MemoryEngineContract.KEY_MESSAGE, "MIDlet runtime is not connected");
+ return result;
+ }
+ try {
+ long token = bridge.getRuntimeToken();
+ int pid = bridge.getTargetPid();
+ int pageSize = bridge.getPageSize();
+ long[] probe = token == 0L ? null : bridge.getReadProbe(token);
+ boolean supported = token != 0L && pid > 0 && pageSize > 0 &&
+ canReadProbe(pid, probe);
+ result.putBoolean(MemoryEngineContract.KEY_SUPPORTED, supported);
+ result.putLong(MemoryEngineContract.KEY_RUNTIME_TOKEN, token);
+ result.putInt(MemoryEngineContract.KEY_TARGET_PID, pid);
+ result.putInt(MemoryEngineContract.KEY_PAGE_SIZE, pageSize);
+ if (!supported) {
+ result.putString(MemoryEngineContract.KEY_MESSAGE, token == 0L
+ ? "No active MIDlet runtime"
+ : "Cross-process memory reads are not supported by this device/runtime");
+ }
+ } catch (RemoteException exception) {
+ result.putBoolean(MemoryEngineContract.KEY_SUPPORTED, false);
+ result.putString(MemoryEngineContract.KEY_MESSAGE, "MIDlet runtime connection was lost");
+ }
+ return result;
+ }
+
+ @Override
+ public void registerCallback(IMemoryEngineCallback callback) {
+ if (callback != null) {
+ callbacks.register(callback);
+ }
+ }
+
+ @Override
+ public void unregisterCallback(IMemoryEngineCallback callback) {
+ if (callback != null) {
+ callbacks.unregister(callback);
+ }
+ }
+
+ @Override
+ public long startKnownSearch(long token, int scope, int type, int predicate,
+ String first, String second) {
+ return enqueue(token, true, scope,
+ () -> NativeMemoryEngine.startKnown(type, predicate, first, second));
+ }
+
+ @Override
+ public long startUnknownSearch(long token, int scope, int type) {
+ return enqueue(token, true, scope, () -> NativeMemoryEngine.startUnknown(type));
+ }
+
+ @Override
+ public long refineKnown(long token, int predicate, String first, String second) {
+ return enqueue(token, false, 0,
+ () -> NativeMemoryEngine.refineKnown(predicate, first, second));
+ }
+
+ @Override
+ public long refineRelative(long token, int predicate, int compareTarget,
+ String first, String second) {
+ return enqueue(token, false, 0,
+ () -> NativeMemoryEngine.refineRelative(predicate, compareTarget, first, second));
+ }
+
+ @Override
+ public long undoSearch(long token) {
+ return enqueue(token, false, 0, NativeMemoryEngine::undo);
+ }
+
+ @Override
+ public long getResultCount(long token) {
+ return isCurrentToken(token) ? NativeMemoryEngine.resultCount() : 0L;
+ }
+
+ @Override
+ public long[] getResultPage(long token, int offset, int limit) {
+ if (!isCurrentToken(token) || offset < 0 || limit <= 0 ||
+ limit > MemoryEngineContract.MAX_RESULT_PAGE_SIZE) {
+ return new long[]{0L};
+ }
+ long[] result = NativeMemoryEngine.resultPage(offset, limit);
+ return result == null ? new long[]{0L} : result;
+ }
+
+ @Override
+ public void clearSearch(long token) {
+ if (isCurrentToken(token)) {
+ worker.execute(NativeMemoryEngine::clear);
+ }
+ }
+
+ @Override
+ public void cancelOperation(long token) {
+ if (token != 0L && (token == configuredToken || isTargetToken(token))) {
+ cancelEpoch.incrementAndGet();
+ NativeMemoryEngine.cancel();
+ }
+ }
+ };
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Intent intent = new Intent(this, MemoryTargetBridgeService.class);
+ targetBound = bindService(intent, targetConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return binder;
+ }
+
+ @Override
+ public void onDestroy() {
+ IMemoryTargetBridge bridge = target;
+ if (bridge != null) {
+ try {
+ bridge.unregisterTargetCallback(targetCallback);
+ } catch (RemoteException ignored) {
+ // The target may already be gone.
+ }
+ }
+ NativeMemoryEngine.cancel();
+ worker.shutdownNow();
+ callbacks.kill();
+ if (targetBound) {
+ unbindService(targetConnection);
+ targetBound = false;
+ }
+ NativeMemoryEngine.clear();
+ super.onDestroy();
+ }
+
+ private long enqueue(long token, boolean configure, int scope, NativeOperation operation) {
+ long operationId = nextOperationId.getAndIncrement();
+ long enqueueEpoch = cancelEpoch.get();
+ worker.execute(() -> {
+ int result;
+ String serviceMessage = null;
+ if (enqueueEpoch != cancelEpoch.get()) {
+ result = MemoryEngineContract.RESULT_CANCELLED;
+ serviceMessage = "Operation cancelled before it started";
+ } else if (token == 0L) {
+ result = MemoryEngineContract.RESULT_NO_SESSION;
+ serviceMessage = "No active MIDlet runtime";
+ } else if (configure) {
+ result = configureTarget(token, scope);
+ if (result == MemoryEngineContract.RESULT_OK) {
+ result = operation.run();
+ } else {
+ serviceMessage = configurationFailureMessage(result);
+ }
+ } else if (!isCurrentToken(token)) {
+ result = MemoryEngineContract.RESULT_TARGET_LOST;
+ serviceMessage = "MIDlet runtime changed or ended";
+ } else {
+ result = operation.run();
+ }
+
+ if (result == MemoryEngineContract.RESULT_OK && !isCurrentToken(token)) {
+ NativeMemoryEngine.clear();
+ configuredToken = 0L;
+ result = MemoryEngineContract.RESULT_TARGET_LOST;
+ serviceMessage = "MIDlet runtime changed during the operation";
+ }
+ notifyFinished(operationId, result, serviceMessage);
+ });
+ return operationId;
+ }
+
+ private int configureTarget(long token, int scope) {
+ if (!MemoryEngineContract.isScope(scope)) {
+ return MemoryEngineContract.RESULT_INVALID_REQUEST;
+ }
+ IMemoryTargetBridge bridge = target;
+ if (bridge == null) {
+ return MemoryEngineContract.RESULT_TARGET_LOST;
+ }
+ try {
+ if (bridge.getRuntimeToken() != token) {
+ return MemoryEngineContract.RESULT_TARGET_LOST;
+ }
+ int pid = bridge.getTargetPid();
+ int pageSize = bridge.getPageSize();
+ if (!canReadProbe(pid, bridge.getReadProbe(token))) {
+ return MemoryEngineContract.RESULT_UNSUPPORTED;
+ }
+ long[] runs = bridge.getResidentRuns(token, scope, MAX_RUNS);
+ if (!MemoryEngineContract.isCompleteRunList(runs)) {
+ if (bridge.getRuntimeToken() != token) {
+ return MemoryEngineContract.RESULT_TARGET_LOST;
+ }
+ return MemoryEngineContract.RESULT_RESOURCE_LIMIT;
+ }
+ int result = NativeMemoryEngine.configureTarget(pid, pageSize, token, runs);
+ if (result == MemoryEngineContract.RESULT_OK) {
+ configuredToken = token;
+ }
+ return result;
+ } catch (RemoteException exception) {
+ return MemoryEngineContract.RESULT_TARGET_LOST;
+ }
+ }
+
+ private boolean isCurrentToken(long token) {
+ if (token == 0L || token != configuredToken) {
+ return false;
+ }
+ return isTargetToken(token);
+ }
+
+ private boolean isTargetToken(long token) {
+ IMemoryTargetBridge bridge = target;
+ if (token == 0L || bridge == null) {
+ return false;
+ }
+ try {
+ return bridge.getRuntimeToken() == token;
+ } catch (RemoteException exception) {
+ return false;
+ }
+ }
+
+ private static boolean canReadProbe(int pid, long[] probe) {
+ return pid > 0 && probe != null && probe.length == 2 && probe[0] > 0L &&
+ NativeMemoryEngine.canReadTarget(pid, probe[0], probe[1]);
+ }
+
+ private void invalidateTarget() {
+ configuredToken = 0L;
+ NativeMemoryEngine.cancel();
+ try {
+ worker.execute(NativeMemoryEngine::clear);
+ } catch (RejectedExecutionException ignored) {
+ // Service teardown already clears native state directly.
+ }
+ }
+
+ private static String configurationFailureMessage(int result) {
+ return switch (result) {
+ case MemoryEngineContract.RESULT_UNSUPPORTED ->
+ "Cross-process memory reads are not supported by this device/runtime";
+ case MemoryEngineContract.RESULT_TARGET_LOST -> "MIDlet runtime changed or ended";
+ case MemoryEngineContract.RESULT_RESOURCE_LIMIT ->
+ "The complete resident range set exceeds the engine resource limit";
+ default -> "Invalid memory engine target configuration";
+ };
+ }
+
+ private void notifyFinished(long operationId, int result, @Nullable String serviceMessage) {
+ long count = result == MemoryEngineContract.RESULT_OK ? NativeMemoryEngine.resultCount() : 0L;
+ String message = serviceMessage == null ? NativeMemoryEngine.lastMessage() : serviceMessage;
+ int callbackCount = callbacks.beginBroadcast();
+ try {
+ for (int index = 0; index < callbackCount; index++) {
+ try {
+ callbacks.getBroadcastItem(index)
+ .onOperationFinished(operationId, result, count, message);
+ } catch (RemoteException ignored) {
+ // RemoteCallbackList removes dead clients.
+ }
+ }
+ } finally {
+ callbacks.finishBroadcast();
+ }
+ }
+
+ private interface NativeOperation {
+ int run();
+ }
+}
diff --git a/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryRuntimeSession.java b/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryRuntimeSession.java
new file mode 100644
index 000000000..2867ed1cc
--- /dev/null
+++ b/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryRuntimeSession.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ru.playsoftware.j2meloader.memory;
+
+import android.os.Process;
+import android.os.SystemClock;
+
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Process-local identity for one live MIDlet runtime, independent from Activity visibility. */
+public final class MemoryRuntimeSession {
+ private static final SecureRandom RANDOM = new SecureRandom();
+ private static final List LISTENERS = new ArrayList<>();
+ private static long activeToken;
+
+ interface Listener {
+ void onRuntimeEnded(long token);
+ }
+
+ private MemoryRuntimeSession() {
+ }
+
+ public static synchronized long start() {
+ if (activeToken != 0L) {
+ return activeToken;
+ }
+ long token;
+ do {
+ token = RANDOM.nextLong()
+ ^ SystemClock.elapsedRealtimeNanos()
+ ^ ((long) Process.myPid() << 32);
+ } while (token == 0L);
+ activeToken = token;
+ return token;
+ }
+
+ public static synchronized long currentToken() {
+ return activeToken;
+ }
+
+ public static synchronized boolean isActive(long token) {
+ return token != 0L && activeToken == token;
+ }
+
+ public static void close(long token) {
+ Listener[] listeners;
+ synchronized (MemoryRuntimeSession.class) {
+ if (token == 0L || activeToken != token) {
+ return;
+ }
+ activeToken = 0L;
+ listeners = LISTENERS.toArray(new Listener[0]);
+ }
+ for (Listener listener : listeners) {
+ try {
+ listener.onRuntimeEnded(token);
+ } catch (RuntimeException ignored) {
+ // Runtime teardown must not be interrupted by an observer.
+ }
+ }
+ }
+
+ static synchronized void addListener(Listener listener) {
+ if (!LISTENERS.contains(listener)) {
+ LISTENERS.add(listener);
+ }
+ }
+
+ static synchronized void removeListener(Listener listener) {
+ LISTENERS.remove(listener);
+ }
+}
diff --git a/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryTargetBridgeService.java b/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryTargetBridgeService.java
new file mode 100644
index 000000000..3eb91e449
--- /dev/null
+++ b/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryTargetBridgeService.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ru.playsoftware.j2meloader.memory;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+
+import androidx.annotation.Nullable;
+
+/** Minimal :midlet bridge for runtime identity and target-local mincore collection. */
+public final class MemoryTargetBridgeService extends Service {
+ private static final int MAX_RUNS = 4096;
+ private static final long[] EMPTY_RUNS = new long[]{0L, 0L};
+ private final Object rangeLock = new Object();
+ private final RemoteCallbackList callbacks = new RemoteCallbackList<>();
+ private final MemoryRuntimeSession.Listener runtimeListener = this::notifyRuntimeEnded;
+
+ private final IMemoryTargetBridge.Stub binder = new IMemoryTargetBridge.Stub() {
+ @Override
+ public void registerTargetCallback(IMemoryTargetCallback callback) {
+ if (callback != null) {
+ callbacks.register(callback);
+ }
+ }
+
+ @Override
+ public void unregisterTargetCallback(IMemoryTargetCallback callback) {
+ if (callback != null) {
+ callbacks.unregister(callback);
+ }
+ }
+
+ @Override
+ public long getRuntimeToken() {
+ return MemoryRuntimeSession.currentToken();
+ }
+
+ @Override
+ public int getTargetPid() {
+ return Process.myPid();
+ }
+
+ @Override
+ public int getPageSize() {
+ return NativeMemoryTarget.pageSize();
+ }
+
+ @Override
+ public long[] getReadProbe(long runtimeToken) {
+ if (!MemoryRuntimeSession.isActive(runtimeToken)) {
+ return new long[0];
+ }
+ long[] probe = NativeMemoryTarget.readProbe();
+ return probe == null ? new long[0] : probe;
+ }
+
+ @Override
+ public long[] getResidentRuns(long runtimeToken, int scope, int maxRuns) {
+ if (!MemoryRuntimeSession.isActive(runtimeToken)
+ || !MemoryEngineContract.isScope(scope)
+ || maxRuns <= 0 || maxRuns > MAX_RUNS) {
+ return EMPTY_RUNS;
+ }
+ synchronized (rangeLock) {
+ long[] runs = NativeMemoryTarget.collectResidentRuns(scope, maxRuns);
+ return runs == null ? EMPTY_RUNS : runs;
+ }
+ }
+ };
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ MemoryRuntimeSession.addListener(runtimeListener);
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return binder;
+ }
+
+ @Override
+ public void onDestroy() {
+ MemoryRuntimeSession.removeListener(runtimeListener);
+ callbacks.kill();
+ super.onDestroy();
+ }
+
+ private void notifyRuntimeEnded(long token) {
+ int count = callbacks.beginBroadcast();
+ try {
+ for (int index = 0; index < count; index++) {
+ try {
+ callbacks.getBroadcastItem(index).onRuntimeEnded(token);
+ } catch (RemoteException ignored) {
+ // RemoteCallbackList removes dead clients.
+ }
+ }
+ } finally {
+ callbacks.finishBroadcast();
+ }
+ }
+}
diff --git a/app/src/main/java/ru/playsoftware/j2meloader/memory/NativeMemoryEngine.java b/app/src/main/java/ru/playsoftware/j2meloader/memory/NativeMemoryEngine.java
new file mode 100644
index 000000000..727084339
--- /dev/null
+++ b/app/src/main/java/ru/playsoftware/j2meloader/memory/NativeMemoryEngine.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ru.playsoftware.j2meloader.memory;
+
+final class NativeMemoryEngine {
+ static {
+ System.loadLibrary("jlmem");
+ }
+
+ private NativeMemoryEngine() {
+ }
+
+ static native int configureTarget(int pid, int pageSize, long runtimeToken, long[] runs);
+
+ static native boolean canReadTarget(int pid, long address, long expectedBits);
+
+ static native int startKnown(int valueType, int predicate, String first, String second);
+
+ static native int startUnknown(int valueType);
+
+ static native int refineKnown(int predicate, String first, String second);
+
+ static native int refineRelative(int predicate, int compareTarget, String first, String second);
+
+ static native int undo();
+
+ static native long resultCount();
+
+ static native long[] resultPage(int offset, int limit);
+
+ static native void clear();
+
+ static native void cancel();
+
+ static native String lastMessage();
+}
diff --git a/app/src/main/java/ru/playsoftware/j2meloader/memory/NativeMemoryTarget.java b/app/src/main/java/ru/playsoftware/j2meloader/memory/NativeMemoryTarget.java
new file mode 100644
index 000000000..b0ab53b6c
--- /dev/null
+++ b/app/src/main/java/ru/playsoftware/j2meloader/memory/NativeMemoryTarget.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ru.playsoftware.j2meloader.memory;
+
+final class NativeMemoryTarget {
+ static {
+ System.loadLibrary("jlmem_target");
+ }
+
+ private NativeMemoryTarget() {
+ }
+
+ static native int pageSize();
+
+ static native long[] readProbe();
+
+ static native long[] collectResidentRuns(int scope, int maxRuns);
+}
diff --git a/app/src/midlet/AndroidManifest.xml b/app/src/midlet/AndroidManifest.xml
index 4eb64d5f6..c84bce99a 100644
--- a/app/src/midlet/AndroidManifest.xml
+++ b/app/src/midlet/AndroidManifest.xml
@@ -25,6 +25,11 @@
+
+