# How @escrcpy/electron-ipcx Handles Function Calls Between Electron Main and Renderer Processes

> Learn how @escrcpy/electron-ipcx enables JavaScript function calls between Electron processes. Explore its serialization, proxy, and routing pipeline for seamless IPC.

- Repository: [viarotel-org/escrcpy](https://github.com/viarotel-org/escrcpy)
- Tags: internals
- Published: 2026-09-10

---

**`@escrcpy/electron-ipcx` extends Electron's native IPC so that JavaScript functions can be passed as arguments across the main/renderer boundary through a three-stage pipeline of serialization, proxy reconstruction, and callback routing via temporary channels.**

`@escrcpy/electron-ipcx` is a core utility in the [viarotel-org/escrcpy](https://github.com/viarotel-org/escrcpy) repository that transparently lifts function arguments from the renderer to the main process (and vice versa) without changing the standard Electron IPC API. The package intercepts `invoke` and `handle` calls, replacing function references with serializable descriptors and reconstructing them as remote proxies on the receiving side.

## The Serialization Pipeline in the Renderer

When the renderer initiates an IPC call using `ipcxRenderer.invoke` or `send`, the library examines every argument for function references that cannot cross the process boundary natively.

### Preparing the Invoke Envelope

In [`packages/electron-ipcx/renderer/index.ts`](https://github.com/viarotel-org/escrcpy/blob/main/packages/electron-ipcx/renderer/index.ts) ([lines 29‑99](https://github.com/viarotel-org/escrcpy/blob/main/packages/electron-ipcx/renderer/index.ts#L29-L99)), the `preparePayload` function calls `serializeArgs` to traverse the argument tree. For each function encountered, the serializer:

- **Replaces the function with `null`** in the sanitized arguments
- **Generates a unique channel name** following the pattern `ipcx_fn_<id>`
- **Records the path** to the function inside the original object using dot notation
- **Creates a descriptor** containing the channel name, path, printable label, and index

The resulting **invoke envelope** contains the sanitized arguments plus an array of these descriptors, making the payload fully serializable for Electron's standard IPC transport.

### Temporary Listener Registration

For every descriptor created, the renderer registers a **one-shot IPC listener** on the generated unique channel ([lines 75‑90](https://github.com/viarotel-org/escrcpy/blob/main/packages/electron-ipcx/renderer/index.ts#L75-L90)). This listener uses `getByPath` (from [`packages/electron-ipcx/shared/paths.ts`](https://github.com/viarotel-org/escrcpy/blob/main/packages/electron-ipcx/shared/paths.ts)) to locate the original function in the argument object and executes it with the arguments forwarded from the main process. These listeners are automatically removed when the promise settles via the `dispose` callback ([lines 79‑91](https://github.com/viarotel-org/escrcpy/blob/main/packages/electron-ipcx/renderer/index.ts#L79-L91)), preventing memory leaks.

## Hydration and Proxy Creation in the Main Process

Once the main process receives the invoke envelope, `ipcxMain.handle` detects the special payload format using validators from [`packages/electron-ipcx/shared/validators.ts`](https://github.com/viarotel-org/escrcpy/blob/main/packages/electron-ipcx/shared/validators.ts) and triggers the hydration phase.

### Reconstructing Functions from Descriptors

In [`packages/electron-ipcx/main/index.ts`](https://github.com/viarotel-org/escrcpy/blob/main/packages/electron-ipcx/main/index.ts) ([lines 33‑55](https://github.com/viarotel-org/escrcpy/blob/main/packages/electron-ipcx/main/index.ts#L33-L55)), the `hydratePayload` function iterates over the descriptor array. For each descriptor, it creates a **proxy function** using `setByPath` (from [`packages/electron-ipcx/shared/paths.ts`](https://github.com/viarotel-org/escrcpy/blob/main/packages/electron-ipcx/shared/paths.ts)) to insert the proxy back into the argument tree at the original path. The proxy, when invoked, forwards its arguments to the renderer by calling `event.sender.send(descriptor.channel, …)`, which triggers the temporary listener created earlier.

### The Callback Routing Mechanism

This round-trip architecture allows the main process handler to execute callbacks synchronously while the actual function body runs asynchronously in the renderer. The channel name (`ipcx_fn_<id>`) acts as a temporary routing key that exists only for the duration of the single IPC invocation, ensuring that callbacks are **location-transparent**—the main process code calls `options.onProgress(i)` exactly as if it were a local function.

## Handling Long-Running Operations with Retained Invokes

Standard `invoke` calls clean up listeners automatically when the promise resolves. For operations that require manual cancellation or extended lifetimes, `ipcxRenderer.invokeRetained` returns both a promise and a `dispose` function.

```typescript
// Renderer – using retained invoke for cancellable operations
import { ipcxRenderer } from '@escrcpy/electron-ipcx/renderer'

const { promise, dispose } = ipcxRenderer.invokeRetained('longTask', {
  onChunk: (data: string) => console.log('chunk', data)
})

// Cancel the operation and clean up listeners
dispose()

```

Calling `dispose()` manually removes all temporary IPC listeners associated with that specific invocation, making it safe to abandon long-running tasks without leaking event handlers.

## Error Handling and Resource Cleanup

Both main and renderer processes wrap function executions using `safeCall` and `wrapError` from [`packages/electron-ipcx/shared/errors.ts`](https://github.com/viarotel-org/escrcpy/blob/main/packages/electron-ipcx/shared/errors.ts). These utilities catch exceptions and serialize them into transferable error objects that preserve stack traces across the process boundary. The renderer's `dispose` implementation ensures that temporary listeners are removed even if the main process throws an error or the renderer promise is rejected, maintaining **deterministic resource cleanup**.

## Summary

- **`serializeArgs`** in the renderer extracts functions into descriptors and replaces them with `null` to create a serializable invoke envelope.
- **`hydratePayload`** in the main process reconstructs these descriptors into proxy functions that route calls back through unique IPC channels (`ipcx_fn_<id>`).
- **Temporary listeners** on the renderer side execute the original callbacks when the main process invokes the proxies.
- **`invokeRetained`** provides manual `dispose` control for long-running operations, preventing listener leaks when operations are cancelled.
- **Error wrappers** ensure exceptions propagate correctly across process boundaries without breaking the IPC contract.

## Frequently Asked Questions

### What types of functions can be passed through @escrcpy/electron-ipcx?

The library supports passing any JavaScript function that can be serialized as a callback, including inline arrow functions and method references. However, functions containing closures over renderer-scoped variables will execute in the renderer context where they were defined, not in the main process. The system uses `getByPath` and `setByPath` to maintain the exact location of these functions within nested argument objects.

### How does electron-ipcx prevent memory leaks from temporary listeners?

Each function descriptor generates a unique channel name (prefixed with `ipcx_fn_<id>`) for a single IPC round-trip. The renderer registers these as one-shot listeners that are automatically removed via the `dispose` callback when the `invoke` promise settles ([lines 79‑91](https://github.com/viarotel-org/escrcpy/blob/main/packages/electron-ipcx/renderer/index.ts#L79-L91)). For retained invocations, manual `dispose()` calls achieve the same cleanup, ensuring no orphaned event listeners accumulate in the renderer process.

### Can multiple callbacks be passed in a single IPC invocation?

Yes. The `serializeArgs` function (in [`packages/electron-ipcx/shared/serialize.ts`](https://github.com/viarotel-org/escrcpy/blob/main/packages/electron-ipcx/shared/serialize.ts)) recursively traverses the entire argument tree and creates a separate descriptor and unique channel for every function found. The main process hydrates each independently, allowing you to pass complex options objects containing multiple callbacks (e.g., `onProgress`, `onError`, `onComplete`) in a single `ipcxRenderer.invoke` call.

### How does this differ from Electron's built-in contextBridge?

While `contextBridge` exposes specific, pre-defined functions from main to renderer during preload, `@escrcpy/electron-ipcx` enables **dynamic callback passing** at runtime through standard `invoke` and `handle` patterns. It does not require pre-exposing APIs in the preload script; instead, it transparently serializes functions as they are passed, making the API identical to Electron's native IPC but with the added capability of **callback lifting** across the process boundary.