C++ API¶
ShirayukiMemory/ is usable on its own — include the umbrella header and link the
core sources into your own tweak.
Every function is in namespace Shirayuki. Nothing here needs Foundation.
Reading and writing¶
// Raw
uint8_t buf[16];
if (Memory::read(addr, buf, sizeof(buf)) != Status::Success) { /* handle */ }
Memory::write(addr, buf, sizeof(buf));
// Typed, reporting failure
float hp = 0;
if (Memory::readValue<float>(addr, hp) == Status::Success) { /* use hp */ }
// Typed, optional
if (auto hp = Memory::tryReadValue<float>(addr)) { /* use *hp */ }
Memory::writeValue<float>(addr, 99999.0f);
Memory::write handles page protection itself: it tries a direct write, and on
failure re-protects the page, writes, restores the original protection and
invalidates the instruction cache.
Always check the status
A failed read leaves the buffer untouched. Treating a zeroed buffer as a real value is how a failed read becomes a zero write.
Regions¶
auto all = Memory::listRegions();
auto rw = Memory::listRegionsFiltered(RegionFilter::ReadWrite);
auto heap = Memory::listRegionsFiltered(RegionFilter::HeapOnly);
auto data = Memory::listRegionsFiltered(RegionFilter::DataOnly);
auto stack = Memory::listRegionsFiltered(RegionFilter::StackOnly);
auto code = Memory::listRegionsFiltered(RegionFilter::Executable);
RegionInfo r = Memory::getRegionInfo(addr);
// r.start, r.size, r.protection, r.userTag, r.label ("MALLOC", "__DATA", …)
// r.isReadable() / isWritable() / isExecutable() / isHeap() / isStack()
HeapOnly matches the kernel's malloc allocator tags; DataOnly matches real
Mach-O __DATA/__DATA_CONST/__DATA_DIRTY segment bounds. An address-range
heuristic cannot work on arm64.
Images and symbols¶
ImageInfo img = Image::find("UnityFramework"); // substring match on the path
if (img.isValid()) {
uintptr_t fn = Image::absoluteAddress(img, 0x123456); // ASLR-independent
}
for (const auto &i : Image::listAll()) { /* i.name, i.base, i.slide */ }
uintptr_t sym = Image::findSymbol("libSystem.B.dylib", "malloc"); // 0 if absent
Scanning¶
All scans read chunked copies through Memory::read. Nothing dereferences target
memory directly, so a concurrent free() in the host app cannot fault the process.
// Typed value — strides by the value width, so only aligned matches
auto hits = Scanner::findValue<float>(region.start, region.size, 99.0f);
// Byte-level, with an explicit stride (1 = unaligned matches too)
uint8_t needle[4] = {0x44, 0x33, 0x22, 0x11};
auto any = Scanner::findValueBytes(start, len, needle, 4, /*stride=*/1);
// IDA-style pattern, ?? for wildcards
auto pat = Scanner::findPattern(start, len, "FF 43 01 D1 ?? ?? ??");
uintptr_t first = Scanner::findPatternFirst(start, len, "FF 43 01 D1");
auto inImg = Scanner::findPatternInImage(img, "FF 43 01 D1 ?? ?? ??");
// Strings
auto strs = Scanner::findString(start, len, "PlayerName");
auto rex = Scanner::findRegex(start, len, "HP:[0-9]+");
// Fuzzy numeric — within ±tolerance
auto fuzzy = Scanner::findValueFuzzy(start, len, 99.0, 0.5, ValueType::Float32);
Narrowing¶
std::vector<Scanner::Candidate> candidates;
for (uintptr_t a : hits) {
Scanner::Candidate c;
c.address = a;
c.snapshotValue.resize(valueTypeSize(ValueType::Int32));
Memory::read(a, c.snapshotValue.data(), c.snapshotValue.size());
candidates.push_back(std::move(c));
}
// … let the app change the value …
auto changed = Scanner::narrowResults(candidates, ValueType::Int32,
CompareMode::Changed);
uint8_t target[kMaxValueSize] = {};
ValueFormat::parse("42", ValueType::Int32, target);
auto exact = Scanner::narrowResults(candidates, ValueType::Int32,
CompareMode::Exact, target);
Modes: Exact Changed Unchanged Increased Decreased GreaterThan
LessThan. The last three need compareValue; the rest ignore it.
For an unknown initial value, seed every aligned slot and narrow by behaviour:
Value types¶
size_t w = valueTypeSize(ValueType::Int64); // 8
std::string s = valueTypeLabel(ValueType::Int64); // "i64" (short)
std::string tag = ValueFormat::toTag(ValueType::Int64); // "int64" (canonical)
ValueType t = ValueFormat::fromTag("i64"); // accepts short and canonical
ValueType out;
if (!ValueFormat::tryFromTag(userInput, out)) { /* report a bad tag */ }
// Parse: returns bytes written, 0 on rejection. Never throws.
uint8_t buf[kMaxValueSize] = {};
if (ValueFormat::parse("-42", ValueType::Int32, buf) == 0) { /* invalid */ }
ValueFormat::format(buf, ValueType::Int32); // "-42" re-parseable
ValueFormat::formatDisplay(buf, ValueType::Int32); // "-42 (0xFFFFFFD6)" display
// Numeric three-way compare. Never use memcmp for this.
int cmp = compareTypedBytes(a, b, ValueType::Float32);
parse rejects empty input, trailing characters and out-of-range values, and
accepts an optional sign plus a 0x prefix.
Patching¶
Patch p = Patch::createWithHex(addr, "1F 20 03 D5");
if (p.isValid() && p.apply()) {
p.originalHex(); // bytes before the patch
p.patchHex(); // bytes written
p.isApplied();
p.restore(); // put the originals back
}
Patch::createNop(addr, 2).apply(); // two ARM64 NOPs
createWithHex snapshots the original bytes, so it must succeed before anything is
written. Keep the Patch object if you want to restore later — discarding it loses
the originals.
Freeze¶
auto &fm = FreezeManager::shared();
uint64_t id = fm.addValue<float>(addr, 99999.0f, "hp");
fm.start(kFreezeIntervalMs); // 16 ms
fm.setAutoIncrement(id, true, 1); // +1 each tick instead of a constant
fm.setActive(id, false); // pause, keep tracking
fm.remove(id);
fm.stop();
// Write only while a threshold condition holds
float clampTo = 50.0f, threshold = 100.0f;
fm.addConditional(addr, &clampTo, sizeof(clampTo), ValueType::Float32,
CompareMode::GreaterThan, &threshold, sizeof(threshold),
[](uint64_t id, uintptr_t a) { /* fired, outside the lock */ });
Watch¶
auto &wm = WatchManager::shared();
uint64_t id = wm.add(addr, ValueType::Float32, "hp");
wm.setCallback([](const WatchEntry &e) {
// e.address, e.previousValue, e.currentValue, e.changeCount, e.hasChanged
});
wm.start(kWatchIntervalMs); // 100 ms
int32_t limit = 10;
wm.setTrigger(id, CompareMode::LessThan, &limit, sizeof(limit),
/*oneShot=*/true, [](const WatchEntry &e) { /* … */ });
entries() returns a copy. Callbacks fire outside the manager lock, so it is safe
to call back into it — but do not assume the entry still exists.
Pointer chains¶
PointerScanConfig cfg;
cfg.targetAddress = addr;
cfg.maxDepth = kPointerScanDefaultDepth;
cfg.maxOffset = 0x1000;
cfg.maxResults = 100;
for (const auto &chain : PointerScanner::scan(cfg)) {
chain.toString(); // "[GameLib + 0xABCD00] → +0x08 → +0x00"
uintptr_t now = chain.resolve(); // 0 if it no longer resolves
PointerScanner::validate(chain, addr);
}
Chains are module-relative, so they survive relaunches and ASLR.
Disassembly and hex¶
for (const auto &insn : Disasm::disassemble(addr, 16)) {
Disasm::formatInstruction(insn); // "STP x29, x30, [sp, #-0x10]!"
}
Hex::toBytes("DE AD BE EF"); // {0xDE, 0xAD, 0xBE, 0xEF}
Hex::fromBytes(bytes); // "DE AD BE EF"
Hex::isValid("DE AD"); // true
Hex::dump(addr, 64); // annotated dump, bounded by
// kMaxHexDumpLength
Sessions¶
Session s;
s.name = "my session";
s.targetBundle = "com.example.game";
Bookmark b{ "player hp", addr, ValueType::Float32, "notes", "combat" };
s.bookmarks.push_back(b);
s.freezeEntries = FreezeManager::shared().entries();
SessionManager::save(s, SessionManager::autoSavePath(s.targetBundle));
Session loaded;
if (SessionManager::load(path, loaded)) { /* … */ }
SessionManager::listSessions();
SessionManager::deleteSession(path);
Addresses persist as hex strings and types as tags, so values above 2^53 and a
future reordering of ValueType both survive a round trip. save writes via a temp
file and rename.
Other modules¶
GroupScan— match a multi-field struct in one pass, e.g.{HP, MP, state}.Snapshot— capture a range and diff two captures at byte level.ReadCache— per-thread TTL cache, for polls tighter than the cache lifetime.ThreadList— enumerate threads with ARM64 PC/SP/LR and run state.Speedhack— API surface only; themach_absolute_timeinterposition is unimplemented.
Configuration¶
All limits live in ShirayukiConfig.hpp: kMaxScanResults, kMaxRegionSize,
kScanChunkSize, kMaxValueSize, kMaxHexDumpLength, kFreezeIntervalMs,
kWatchIntervalMs, the pointer-scan defaults and the worker interval clamps.