Skip to content

Replace BugpunchClient UnitySendMessage receivers with native callbacks (AndroidJavaProxy + P/Invoke) #48

@oddgames-david

Description

@oddgames-david

Spun off from #41 — the lifecycle conversion's remaining piece.

Problem

`BugpunchClient.cs` still inherits `MonoBehaviour` for one reason: 10 `UnitySendMessage` receiver methods that native Java + Obj-C++ call into. Without these, BugpunchClient could be a plain static-state holder.

Receivers in BugpunchClient.cs:

  • `OnPollUpgradeRequested(string)` — native poller flips upgradeToWebSocket flag.
  • `OnShowChatBoardRequested(string)` — legacy native shell entry.
  • `OnRecordingBarChatTapped(string)` — native recording bar chat icon.
  • `OnChatBannerDismissed(string)` — native chat banner X tap.
  • `OnChatBannerOpened(string)` — native chat banner body tap.
  • `OnRecordingBarFeedbackTapped(string)` — native recording bar feedback icon.
  • `DirectiveRunScript(string)` — native crash-directive run_script action.
  • `OnPollScripts(string)` — poll response with scheduled scripts.
  • `OnApprovedScriptRequest(string)` — native chat scriptRequest approval.
  • `OnApprovedDataRequest(string)` — native chat dataRequest approval.

Plus `gameObject.AddComponent()` in the streamer lazy-init path needs a host GameObject for the streamer MonoBehaviour.

Why split off

#41 closed the four big lifecycle conversions:

  • `Update` → PlayerLoop entry.
  • `OnApplicationPause` → `Application.focusChanged`.
  • `OnDestroy` → `Application.quitting` + idempotent `Teardown()`.
  • 4 `StartCoroutine` poll loops → async Tasks.

Plus three siblings (`BugpunchSceneTick`, `BugpunchSurfaceRecorder`, `CrashDirectiveHandler`) fully converted to static classes.

The receiver migration is a different shape of work — coordinated native + C# changes — and needs per-Unity-version validation that's better scoped on its own.

Scope

Android (AndroidJavaProxy)

For each receiver, replace native-side calls of `BugpunchUnity.sendMessage("BugpunchClient", "X", json)` with a Java callback interface that the C# side registers via `AndroidJavaProxy`.

Sketch:

// In BugpunchPoller.java / BugpunchChatBanner.java / BugpunchReportOverlay.java etc.
public interface BugpunchCallbacks {
    void onPollUpgradeRequested();
    void onChatBannerDismissed();
    void onChatBannerOpened();
    void directiveRunScript(String payload);
    // ... one method per receiver ...
}

private static volatile BugpunchCallbacks sCallbacks;

public static void registerCallbacks(BugpunchCallbacks cb) {
    sCallbacks = cb;
}

// Call site replaces BugpunchUnity.sendMessage(\"BugpunchClient\", \"DirectiveRunScript\", payload):
BugpunchCallbacks cb = sCallbacks;
if (cb != null) cb.directiveRunScript(payload);

C# side:

class BugpunchCallbacks : AndroidJavaProxy
{
    public BugpunchCallbacks() : base(\"au.com.oddgames.bugpunch.BugpunchCallbacks\") { }
    void onPollUpgradeRequested() => BugpunchClient.HandleUpgradeToWebSocket();
    void onChatBannerDismissed() => BugpunchPoller.NotifyBannerDismissed();
    void onChatBannerOpened() => BugpunchPoller.NotifyBannerOpened();
    void directiveRunScript(string payload) => CrashDirectiveHandler.RunScript(payload);
    // ...
}

Risk: AndroidJavaProxy under IL2CPP has known historical bugs (delegate marshalling, method resolution, threading) that need per-Unity-version validation. Targets to confirm: 2022.3 LTS, 6000.0 LTS, 6000.x current.

iOS (P/Invoke callbacks)

For each receiver, the existing `UnitySendMessage("BugpunchClient", "X", json)` calls become P/Invoke callback dispatches. The C# side registers a static delegate at startup; the native side stores the function pointer and calls it directly.

Sketch:

// In BugpunchPoller.mm / BugpunchChatBanner.mm etc.
typedef void (*BPCallbackString)(const char* payload);
typedef void (*BPCallbackVoid)(void);

static BPCallbackVoid s_onPollUpgrade;
static BPCallbackString s_directiveRunScript;
// ... one per receiver ...

extern \"C\" void Bugpunch_RegisterCallbacks(
    BPCallbackVoid onPollUpgrade,
    BPCallbackVoid onChatBannerDismissed,
    BPCallbackVoid onChatBannerOpened,
    BPCallbackString directiveRunScript,
    // ...
) {
    s_onPollUpgrade = onPollUpgrade;
    // ...
}

C# side (P/Invoke decls + delegate registration):

[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void BPVoidCallback();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void BPStringCallback(IntPtr utf8);

[DllImport(\"__Internal\")] static extern void Bugpunch_RegisterCallbacks(
    BPVoidCallback onPollUpgrade,
    BPVoidCallback onChatBannerDismissed,
    BPVoidCallback onChatBannerOpened,
    BPStringCallback directiveRunScript
    // ...
);

[AOT.MonoPInvokeCallback(typeof(BPVoidCallback))]
static void OnPollUpgrade() => BugpunchClient.HandleUpgradeToWebSocket();

[AOT.MonoPInvokeCallback(typeof(BPStringCallback))]
static void DirectiveRunScript(IntPtr payloadPtr)
{
    var payload = Marshal.PtrToStringUTF8(payloadPtr);
    CrashDirectiveHandler.RunScript(payload);
}

Risk: `MonoPInvokeCallback` requires static methods. State-bearing callbacks need to look up the active client via `BugpunchClient.Instance` (or a static dispatcher). Already true after #41's lifecycle conversions made everything static-friendly.

C# side cleanup

Once both lanes register callbacks instead of sending Unity messages:

  • Drop `: MonoBehaviour` from `BugpunchClient`. Convert remaining instance fields to static. Adjust `Initialize` to allocate a regular C# object instead of `AddComponent`.
  • The host GameObject still exists (the IDE service MonoBehaviours `HierarchyService`, `WebRTCStreamer`, etc. need a host for `AddComponent`). Rename it to `[Bugpunch Services Host]` and decouple BugpunchClient from it.
  • `Disconnect()` keeps a static reference to the host GameObject for explicit teardown via `UnityEngine.Object.Destroy`.

Out of scope

  • Migrating the IDE service MonoBehaviours (`HierarchyService`, `WebRTCStreamer`, etc.) themselves to static classes. Those are insulated from QA-script Destroy by living on a shared host GameObject; can be a follow-up if the AddComponent host bothers anyone.
  • Per-receiver behavioural changes — strict 1:1 migration. Any UX tweaks are separate.

Acceptance criteria

  • All 10 receivers in BugpunchClient.cs replaced with native callback registration paths (Android + iOS).
  • BugpunchClient drops `: MonoBehaviour`, becomes static-state-bearing class.
  • Verified on Unity 2022.3 LTS, 6000.0 LTS, 6000.x current — IL2CPP build, AndroidJavaProxy + MonoPInvokeCallback both wired and functional.
  • No regression in: poll-mode upgrade, chat banner dismiss/open, recording-bar chat / feedback taps, directive run-script, scheduled-script execution, approved chat script / data requests.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions