Skip to content

mobile: return string instead of []byte + update Swift callers#8663

Merged
myleshorton merged 2 commits intogarmr/radiance-daemon-refactorfrom
fix/gomobile-return-string-refactor
Apr 16, 2026
Merged

mobile: return string instead of []byte + update Swift callers#8663
myleshorton merged 2 commits intogarmr/radiance-daemon-refactorfrom
fix/gomobile-return-string-refactor

Conversation

@myleshorton
Copy link
Copy Markdown
Contributor

Summary

Gomobile-exported functions returning `([]byte, error)` can crash with a Go runtime write-barrier panic during GC. This PR converts them to return `(string, error)` and updates the macOS + iOS Swift callers accordingly.

Root Cause

The gomobile wrapper runs on the cgo callback goroutine (locked to the C thread). After the Go function returns, the wrapper copies the return value into a C struct on the OS thread stack using `runtime.wbMove`. Because `[]byte` contains a pointer (slice data), `wbMove` calls `bulkBarrierPreWrite` to track the write. With an active GC cycle, `bulkBarrierPreWrite` verifies the destination is GC-tracked — the C thread stack is not — and calls `runtime.throw`.

Full analysis: getlantern/engineering#3175 (from Freshdesk #172640 — Derek's crash on macOS 26.3.1).

Note: `radiance/common.RunOffCgoStack` (the `withCoreR` wrapper) was intended to fix this class of bug, but it only moves the function body to a worker goroutine — the return-value copy still happens on the cgo callback goroutine, so it's still vulnerable for pointer-bearing return types.

Go changes (`lantern-core/mobile/mobile.go`)

11 functions converted from `([]byte, error)` or `[]byte` to `(string, error)` or `string`:

  • `AvailableFeatures`
  • `UserData`, `FetchUserData`
  • `GetAvailableServers`, `GetSelectedServerJSON`
  • `OAuthLoginCallback`
  • `AcknowledgeGooglePurchase`, `AcknowledgeApplePurchase`
  • `Login`, `Logout`, `DeleteAccount`

Swift changes (`macos/` + `ios/` MethodHandler)

The Dart side reads some method results via `jsonDecode(utf8.decode(bytes))` and others via `jsonDecode(result)` (string). To avoid touching the Dart side:

  • For methods whose Dart side reads bytes: convert `String?` back to `Data` in Swift via `.data(using: .utf8)` before passing to `result()`. Applies to `getUserData`, `fetchUserData`, `oauthLoginCallback`, `login`, `logout`, `deleteAccount`, `acknowledgeInAppPurchase`.
  • For methods whose Dart side expects String: drop the old `String(data: ..., encoding: .utf8)` conversion and pass the gomobile string directly. Applies to `featureFlags`, `getLanternAvailableServers`, `getSelectedServerJSON`.

Test Plan

  • `go build ./...` passes in lantern repo
  • Swift builds + links against regenerated `Liblantern` (the `Mobile.objc.h` header in `macos/Frameworks/Liblantern.xcframework/` needs to be regenerated by the gomobile build for the new signatures)
  • End-to-end: user data flow, login/logout, purchase flow all work on macOS beta
  • Verify crash no longer reproduces in next beta

Target branch

Targeting `garmr/radiance-daemon-refactor` per discussion in Slack — the same fix landed on `radiance` main as getlantern/radiance#416 (lock + telemetry), and this is the client-side half.

🤖 Generated with Claude Code

The gomobile wrapper copies Go pointer-containing return values to the C
thread stack using runtime.wbMove. When a GC cycle runs during the copy,
bulkBarrierPreWrite panics because the destination isn't GC-tracked.
Returning string avoids this — gomobile marshals strings via C heap
allocation rather than leaving them as Go slice headers.

See getlantern/engineering#3175 for the full crash analysis (from
Freshdesk #172640 — Derek reporting "Lantern Crash" on macOS 26.3.1).

Go changes:
  AvailableFeatures, UserData, FetchUserData, GetAvailableServers,
  GetSelectedServerJSON, OAuthLoginCallback, AcknowledgeGooglePurchase,
  AcknowledgeApplePurchase, Login, Logout, DeleteAccount

Swift changes (macos + ios): preserve Flutter contract by converting
the string back to Data for methods whose Dart side reads `bytes` via
utf8.decode (getUserData, fetchUserData, oauthLoginCallback, login,
logout, deleteAccount, acknowledgeInAppPurchase). For methods whose Dart
side expects String (featureFlags, getLanternAvailableServers,
getSelectedServerJSON), just pass the gomobile string directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR mitigates a gomobile GC/write-barrier crash by changing exported Go mobile APIs to return string (instead of []byte) and updating the iOS/macOS Swift method-channel handlers to preserve existing Flutter/Dart contracts (bytes vs string expectations).

Changes:

  • Update 11 gomobile-exported functions in lantern-core/mobile/mobile.go from []byte / ([]byte, error) to string / (string, error).
  • Update iOS/macOS MethodHandler.swift call sites to either (a) convert returned JSON String back to Data for Dart paths that expect bytes, or (b) pass through String directly where Dart expects a string.
  • Normalize Swift-side fallback behavior for empty results (e.g., "{}" / "[]") for feature flags and server JSON methods.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
lantern-core/mobile/mobile.go Switch exported gomobile return types to string to avoid pointer-bearing return values on cgo callback stacks.
ios/Runner/Handlers/MethodHandler.swift Update Swift callers to match new gomobile signatures while preserving Dart decoding expectations.
macos/Runner/Handlers/MethodHandler.swift Same as iOS: adapt to string returns and maintain Dart-side contract (bytes vs string).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lantern-core/mobile/mobile.go
@myleshorton
Copy link
Copy Markdown
Contributor Author

It sounds like you have another solution for this @garmr-ulfr is that right? Should I close this?

@garmr-ulfr
Copy link
Copy Markdown
Contributor

It sounds like you have another solution for this @garmr-ulfr is that right? Should I close this?

I'm still working on it. It's fine to merge this in for now.

The gomobile-exported funcs in lantern-core/mobile/mobile.go now return
string instead of []byte. The generated Android binding will therefore
return String where it used to return ByteArray.

For each affected method, match what the iOS handler does so the Flutter
platform-channel contract stays stable:

  * Methods whose Dart callers expect bytes (Uint8List) — login,
    logout, deleteAccount, userData, fetchUserData, oauthLoginCallback,
    acknowledgeGooglePurchase — convert the String result via
    `.toByteArray(Charsets.UTF_8)` before calling success() (mirrors
    Swift's `.data(using: .utf8)`).

  * Methods whose Dart callers expect a String — availableFeatures,
    getAvailableServers, getSelectedServerJSON — drop the
    `String(byteArray)` constructor and use the return value directly,
    with the same "{}" / "[]" empty-default that iOS uses.

Addresses Copilot review on PR #8663.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@myleshorton
Copy link
Copy Markdown
Contributor Author

OK let's see if this solves the issue for @Derekf5. That will at least give us another data point to work with.

@myleshorton myleshorton merged commit 19f2fa8 into garmr/radiance-daemon-refactor Apr 16, 2026
1 check passed
@myleshorton myleshorton deleted the fix/gomobile-return-string-refactor branch April 16, 2026 20:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants