Implementing Ipc Interfaces

This article discusses IPC communication in iTwin.js. See also RPC vs IPC.

Overview

IPC (Inter-Process Communication)) is a direct-connect communication technique used in iTwin.js when the frontend and backend processes are paired one-to-one. IPC is most commonly used for native apps where both processes are always on the same computer, but may also be used by web apps with a "dedicated" backend.

IpcSocket Interface

The basis for communication between the frontend/backend pair is a platform-specific implementation of the IpcSocket interface. There are two specializations of IpcSocket, IpcSocketFrontend and IpcSocketBackend for the frontend and backend respectively. They both allow sending and receiving messages, but frontend has a method to invoke functions on the backend, and the backend interface has a method to handle those invocations.

For desktops, those interfaces are implemented by Electron's ipc layers, exposed through the @itwin/core-electron package.

To use Ipc under Electron, you must call ElectronApp.startup on the frontend and ElectronHost.startup on the backend:

import { ElectronHost } from "@itwin/core-electron/lib/cjs/ElectronBackend";

export async function initializeForElectron(rpcInterfaces: RpcInterfaceDefinition[]) {
  await ElectronHost.startup({ electronHost: { rpcInterfaces } });
}

and


export async function initializeElectron(rpcInterfaces: RpcInterfaceDefinition[]) {
  await ElectronApp.startup({
    iModelApp: {
      rpcInterfaces,
      localization: new EmptyLocalization(),
    },
  });
}

For mobile devices those interfaces are implemented over WebSockets.

TODO: mobile initialization

IpcApp and IpcHost

Generally, iTwin.js programmers won't need to work with the low-level IpcSocket interface.

On the frontend, the class IpcApp must be initialized at startup with the platform-specific implementation of IpcSocketFrontend and contains the method IpcApp.addListener to supply a handler for notification messages sent from the backend to the frontend.

On the backend, the class IpcHost must be initialized at startup with the platform-specific implementation of IpcSocketBackend and contains the method IpcHost.send to send a notification message from the backend to the frontend.

Since Ipc is only enabled in situations where a dedicated backend is available, each of IpcApp and IpcHost have an isValid method that may be tested from code designed to work with or without Ipc.

Creating Your own Ipc Interfaces and IpcHandlers

To enable type-safe cross-process method calls using IPC, there are three required pieces:

  1. Define the method signatures in an interface. This must be in a file that can be imported from both your frontend code and backend code. In iTwin.js we use the convention of a folder named common for this purpose. Note that all methods in your interface must return a Promise. In the same file, define a variable that has a string with a unique name for the ipc channel your interface will use. If you'd like, you can incorporate a version identifier in the channel name (e.g. append "-1").
const myChannel = "my-interface-v1.2";

interface MyInterface {
  sayHello(arg1: string, arg2: number, arg3: boolean): Promise<string>;
}
  1. In your backend code, implement a class that extends IpcHandler and implements the interface you defined in step 1. In your startup code, call the static method register on your new class. Your class must implement the abstract method get channelName(). Return the channel name variable from your interface file.

To ensure that private methods in your new class are inaccessible from the frontend, make sure to either move them out of the class or define them using the hash # prefix.

class MyClassHandler extends IpcHandler implements MyInterface {
  public get channelName() {
    return myChannel;
  }
  public async sayHello(arg1: string, arg2: number, arg3: boolean) {
    return `hello: ${arg1} ${arg2} ${arg3}`;
  }
  #privateSayHello(arg1: string) {
    return `hello: ${arg1}`;
  }
}

// ...in startup code
MyClassHandler.register();
  1. In your frontend code, make an Proxy object using IpcApp.makeIpcProxy:
const myBackendIpc = IpcApp.makeIpcProxy<MyInterface>(myChannel);

This makes a Proxy object to call the methods of MyInterface from the frontend.

const hello = await myBackendIpc.sayHello("abc", 10, true);

Note that all IPC methods return a Promise, so their return value must be awaited.

As you refine your interface, you may decide to change your channel name.

Your application can have as many Ipc Interfaces as you like (each must have a unique channel name), and each of your interfaces may have as many functions as you need.

Last Updated: 14 November, 2024