Skip to content

Commit 161cc3e

Browse files
feat: injectee
1 parent e7dc253 commit 161cc3e

14 files changed

Lines changed: 653 additions & 13 deletions

File tree

.github/workflows/build.yml

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ name: Build & Test
33
on:
44
push:
55
branches: [main, master]
6+
tags: ['v*']
67
pull_request:
78
branches: [main, master]
89

@@ -14,19 +15,24 @@ jobs:
1415
include:
1516
- os: macos-latest
1617
arch: arm64
17-
name: macOS ARM64
18+
name: macos-arm64
1819
- os: macos-15-intel
1920
arch: x64
20-
name: macOS x64
21+
name: macos-x64
2122
- os: ubuntu-24.04
2223
arch: x64
23-
name: Linux x64
24+
name: linux-x64
2425
- os: ubuntu-24.04-arm
2526
arch: arm64
26-
name: Linux ARM64
27+
name: linux-arm64
2728
- os: windows-latest
2829
arch: x64
29-
name: Windows x64
30+
name: windows-x64
31+
# Android cross-compile on Linux x64
32+
- os: ubuntu-24.04
33+
arch: arm64
34+
name: android-arm64
35+
android: true
3036

3137
runs-on: ${{ matrix.os }}
3238
name: ${{ matrix.name }}
@@ -40,25 +46,29 @@ jobs:
4046
xmake-version: 3.0.7
4147

4248
- name: Setup pnpm
49+
if: ${{ !matrix.android }}
4350
uses: pnpm/action-setup@v4
4451
with:
4552
version: 10
4653

4754
- name: Setup Node.js
55+
if: ${{ !matrix.android }}
4856
uses: actions/setup-node@v4
4957
with:
5058
node-version: 22
5159

5260
- name: Install TS dependencies
61+
if: ${{ !matrix.android }}
5362
working-directory: src/core/typescript
5463
run: pnpm install
5564

5665
- name: Build TypeScript
66+
if: ${{ !matrix.android }}
5767
working-directory: src/core/typescript
5868
run: pnpm build
5969

60-
- name: Install GCC 15 (Linux)
61-
if: runner.os == 'Linux'
70+
- name: Install GCC 15 (Linux native)
71+
if: runner.os == 'Linux' && !matrix.android
6272
run: |
6373
sudo apt-get update
6474
sudo apt-get install -y software-properties-common
@@ -68,18 +78,54 @@ jobs:
6878
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-15 100
6979
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-15 100
7080
71-
- name: Configure xmake
72-
if: runner.os == 'Linux'
81+
- name: Setup Android NDK
82+
if: ${{ matrix.android }}
83+
uses: nttld/setup-ndk@v1
84+
id: setup-ndk
85+
with:
86+
ndk-version: r27c
87+
88+
- name: Configure xmake (Android)
89+
if: ${{ matrix.android }}
90+
run: |
91+
xmake f -p android --ndk=${{ steps.setup-ndk.outputs.ndk-path }} --ndk_sdkver=21 -a arm64-v8a -m releasedbg -v -y
92+
93+
- name: Configure xmake (Linux native)
94+
if: runner.os == 'Linux' && !matrix.android
7395
run: |
7496
xmake f -m releasedbg -v -y --toolchain=gcc
75-
76-
- name: Configure xmake
97+
98+
- name: Configure xmake (non-Linux)
7799
if: runner.os != 'Linux'
78100
run: |
79101
xmake f -m releasedbg -v -y
80102
81-
- name: Build
103+
- name: Build (Android - injectee only)
104+
if: ${{ matrix.android }}
105+
run: xmake build -y chromatic-injectee
106+
107+
- name: Build (native)
108+
if: ${{ !matrix.android }}
82109
run: xmake build -y
83110

84111
- name: Test
112+
if: ${{ !matrix.android }}
85113
run: xmake run chromatic-test
114+
115+
- name: Prepare artifacts
116+
shell: bash
117+
run: |
118+
mkdir -p artifacts
119+
if [ "${{ runner.os }}" = "Windows" ]; then
120+
find build -name "chromatic-injectee.dll" -exec cp {} artifacts/ \;
121+
elif [ "${{ runner.os }}" = "macOS" ]; then
122+
find build -name "libchromatic-injectee.dylib" -exec cp {} artifacts/ \;
123+
else
124+
find build -name "libchromatic-injectee.so" -exec cp {} artifacts/ \;
125+
fi
126+
127+
- name: Upload Artifact
128+
uses: actions/upload-artifact@v4
129+
with:
130+
name: chromatic-injectee-${{ matrix.name }}
131+
path: artifacts/*

src/core/bindings/binding_types.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@
1313
#include "native_breakpoint.h"
1414
#include "native_hw_breakpoint.h"
1515
#include "native_memory_access_monitor.h"
16+
#include "script_lifecycle.h"

src/core/bindings/generated_bindings/binding_qjs.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,30 @@ template<> struct js_bind<chromatic::js::NativeMemoryAccessMonitor> {
685685
}
686686
};
687687

688+
template <> struct qjs::js_traits<chromatic::js::ScriptLifecycle> {
689+
static chromatic::js::ScriptLifecycle unwrap(JSContext *ctx, JSValueConst v) {
690+
chromatic::js::ScriptLifecycle obj;
691+
692+
return obj;
693+
}
694+
695+
static JSValue wrap(JSContext *ctx, const chromatic::js::ScriptLifecycle &val) noexcept {
696+
JSValue obj = JS_NewObject(ctx);
697+
698+
return obj;
699+
}
700+
};
701+
template<> struct js_bind<chromatic::js::ScriptLifecycle> {
702+
static void bind(qjs::Context::Module &mod) {
703+
mod.class_<chromatic::js::ScriptLifecycle>("ScriptLifecycle")
704+
.constructor<>()
705+
.static_fun<&chromatic::js::ScriptLifecycle::onDispose>("onDispose")
706+
.static_fun<&chromatic::js::ScriptLifecycle::removeDisposeCallback>("removeDisposeCallback")
707+
.static_fun<&chromatic::js::ScriptLifecycle::removeAllDisposeCallbacks>("removeAllDisposeCallbacks")
708+
;
709+
}
710+
};
711+
688712
inline void chromatic_bindAll(qjs::Context::Module &mod) {
689713

690714
js_bind<chromatic::js::console>::bind(mod);
@@ -725,4 +749,6 @@ inline void chromatic_bindAll(qjs::Context::Module &mod) {
725749

726750
js_bind<chromatic::js::NativeMemoryAccessMonitor>::bind(mod);
727751

752+
js_bind<chromatic::js::ScriptLifecycle>::bind(mod);
753+
728754
}

src/core/bindings/generated_bindings/binding_types.d.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,5 +709,25 @@ export class NativeMemoryAccessMonitor {
709709
*/
710710
static drainPending(): number
711711
}
712+
export class ScriptLifecycle {
713+
/**
714+
* Register a callback to be called before script dispose/reload.
715+
* Returns callbackId (hex) for removal.
716+
* @param callback: (() => void)
717+
* @returns string
718+
*/
719+
static onDispose(callback: (() => void)): string
720+
/**
721+
* Remove a dispose callback by ID.
722+
* @param callbackId: string
723+
* @returns void
724+
*/
725+
static removeDisposeCallback(callbackId: string): void
726+
/**
727+
* Remove all dispose callbacks.
728+
@returns void
729+
*/
730+
static removeAllDisposeCallbacks(): void
731+
}
712732
}
713733

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#include "script_lifecycle.h"
2+
#include <cstdint>
3+
#include <mutex>
4+
#include <sstream>
5+
#include <unordered_map>
6+
#include <vector>
7+
#include <fmt/core.h>
8+
9+
namespace {
10+
11+
static std::mutex g_mutex;
12+
static uint64_t g_nextId = 1;
13+
static std::unordered_map<uint64_t, std::function<void()>> g_callbacks;
14+
15+
std::string toHexId(uint64_t id) {
16+
std::ostringstream oss;
17+
oss << "0x" << std::hex << id;
18+
return oss.str();
19+
}
20+
21+
} // namespace
22+
23+
namespace chromatic::js {
24+
25+
std::string ScriptLifecycle::onDispose(std::function<void()> callback) {
26+
std::lock_guard lock(g_mutex);
27+
auto id = g_nextId++;
28+
g_callbacks[id] = std::move(callback);
29+
return toHexId(id);
30+
}
31+
32+
void ScriptLifecycle::removeDisposeCallback(const std::string &callbackId) {
33+
uint64_t id = std::stoull(callbackId, nullptr, 16);
34+
std::lock_guard lock(g_mutex);
35+
g_callbacks.erase(id);
36+
}
37+
38+
void ScriptLifecycle::removeAllDisposeCallbacks() {
39+
std::lock_guard lock(g_mutex);
40+
g_callbacks.clear();
41+
}
42+
43+
void ScriptLifecycle::_callDisposeCallbacks() {
44+
// Take a snapshot of callbacks under lock, then call them outside lock
45+
std::vector<std::function<void()>> callbacks;
46+
{
47+
std::lock_guard lock(g_mutex);
48+
callbacks.reserve(g_callbacks.size());
49+
for (auto &[id, cb] : g_callbacks) {
50+
callbacks.push_back(cb);
51+
}
52+
}
53+
54+
for (auto &cb : callbacks) {
55+
try {
56+
cb();
57+
} catch (const std::exception &e) {
58+
fmt::print(stderr, "Exception in dispose callback: {}\n", e.what());
59+
} catch (...) {
60+
fmt::print(stderr, "Unknown exception in dispose callback\n");
61+
}
62+
}
63+
64+
// Clear all callbacks after calling
65+
removeAllDisposeCallbacks();
66+
}
67+
68+
} // namespace chromatic::js
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#pragma once
2+
#include <functional>
3+
#include <string>
4+
5+
namespace chromatic::js {
6+
7+
struct ScriptLifecycle {
8+
/// Register a callback to be called before script dispose/reload.
9+
/// Returns callbackId (hex) for removal.
10+
static std::string onDispose(std::function<void()> callback);
11+
12+
/// Remove a dispose callback by ID.
13+
static void removeDisposeCallback(const std::string &callbackId);
14+
15+
/// Remove all dispose callbacks.
16+
static void removeAllDisposeCallbacks();
17+
18+
/// [Internal] Call all registered dispose callbacks, then clear them.
19+
static void _callDisposeCallbacks();
20+
};
21+
22+
} // namespace chromatic::js

src/core/script.cc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "bindings/native_hw_breakpoint.h"
66
#include "bindings/native_interceptor.h"
77
#include "bindings/native_memory_access_monitor.h"
8+
#include "bindings/script_lifecycle.h"
89
#include "fmt/base.h"
910

1011
extern "C" {
@@ -18,6 +19,7 @@ std::string index_js = {(const char *)_binary_index_js_start,
1819
namespace chromatic::script {
1920
void runtime::cleanup() {
2021
// Auto-cleanup all subsystems when the script context is disposed
22+
chromatic::js::ScriptLifecycle::removeAllDisposeCallbacks();
2123
chromatic::js::NativeMemoryAccessMonitor::disableAll();
2224
chromatic::js::NativeHardwareBreakpoint::removeAll();
2325
chromatic::js::NativeSoftwareBreakpoint::removeAll();
@@ -26,7 +28,9 @@ void runtime::cleanup() {
2628
chromatic::js::NativeExceptionHandler::disable();
2729
}
2830
void runtime::reset() {
29-
// Auto-cleanup before resetting the JS runtime
31+
// Let JS do its cleanup first via dispose callbacks
32+
chromatic::js::ScriptLifecycle::_callDisposeCallbacks();
33+
// Then native cleanup
3034
cleanup();
3135
context.on_bind.clear();
3236
context.on_bind.push_back(

src/core/typescript/src/main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Interceptor } from './interceptor/index';
1515
import { ExceptionHandler } from './exception-handler';
1616
import { SoftwareBreakpoint, HardwareBreakpoint } from './breakpoint';
1717
import { MemoryAccessMonitor } from './memory-access-monitor';
18+
import { Script } from './script-lifecycle';
1819

1920
// Register globals (Frida-compatible)
2021
const g = globalThis as any;
@@ -37,6 +38,9 @@ g.SoftwareBreakpoint = SoftwareBreakpoint;
3738
g.HardwareBreakpoint = HardwareBreakpoint;
3839
g.MemoryAccessMonitor = MemoryAccessMonitor;
3940

41+
// Script lifecycle
42+
g.Script = Script;
43+
4044
// Utility functions
4145
g.ptr = ptr;
4246
g.NULL = NULL;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ScriptLifecycle as NativeScriptLifecycle } from 'chromatic';
2+
3+
export const Script = {
4+
/**
5+
* Register a callback to be called before script dispose/reload.
6+
* Returns a disposable handle with a `remove()` method.
7+
*
8+
* @example
9+
* const handle = Script.onDispose(() => {
10+
* Interceptor.detachAll();
11+
* console.log('Cleaning up before reload...');
12+
* });
13+
* // Later, to unregister:
14+
* handle.remove();
15+
*/
16+
onDispose(callback: () => void): { remove: () => void } {
17+
const id = NativeScriptLifecycle.onDispose(callback);
18+
return {
19+
remove() {
20+
NativeScriptLifecycle.removeDisposeCallback(id);
21+
},
22+
};
23+
},
24+
};

0 commit comments

Comments
 (0)