Unity WebView RPC provides an abstraction layer that allows communication between the Unity client (C#) and WebView (HTML, JS) using protobuf, similar to gRPC.
It extends the traditional JavaScript bridge
communication method to work similarly to a Remote Procedure Call (RPC).
To avoid dependency on a specific WebView library, it provides a Bridge interface so that communication can be achieved with the same code, regardless of the WebView library used.
WebView RPC v2.0 introduces significant improvements for robustness, performance, and memory management.
- Unique Request IDs (Breaking Change):
RequestId
now uses UUIDs instead of incremental numbers, preventing collisions when using multiple WebViews. - Smarter Chunking (Breaking Change): The
maxChunkSize
setting now accurately represents the final Base64-encoded message size, including all overhead. New utility methods likegetEffectivePayloadSize()
help you configure chunking precisely. - Improved Timeout Handling: Fixed a critical bug where requests could wait indefinitely. Incomplete messages now correctly time out.
- Resource Management: Added
dispose()
methods to RPC clients, servers, and bridges to prevent memory leaks from event listeners.
For a complete list of changes, see the CHANGELOG.md for the app-webview-rpc
library.
WebView RPC simplifies the workflow compared to the traditional JavaScript bridge
method.
flowchart LR
subgraph Traditional Method
direction LR
A[Unity C#] <--> B[Data Class<br>& Manual Method Implementation]
B <--> C[JSON Parser]
C <--> D[JavaScript Bridge]
D <--> E[JSON Parser]
E <--> F[Data Class<br>& Manual Method Implementation]
F <--> G[JavaScript WebView]
end
subgraph WebView RPC Method
direction LR
H[Unity C#<br>Direct Method Call] <--> I[Magic Space]
I <--> J[JavaScript<br>Direct Method Call]
end
Internally, WebView RPC is structured as follows:
flowchart LR
A[Unity C# Direct Call] <--> B[protobuf-generated C# Code] <--> C[protobuf Serializer/Deserializer] <--> D[Base64 Serializer/Deserializer] <--> E[JavaScript Bridge] <--> F[Base64 Serializer/Deserializer] <--> G[protobuf Serializer/Deserializer] <--> H[protobuf-generated JavaScript Code] <--> I[JavaScript Direct Call]
- Unity C# Direct Call
- Calls an RPC interface function like a regular method in Unity.
- protobuf-generated C# Code
- Auto-generated C# wrapper/stub from the proto definition.
- RPC methods and data structures are based on protobuf.
- Base64 Serializer + JavaScript Bridge
- Converts raw byte data to Base64 before sending it through the WebView (browser).
- JavaScript receives the same format.
- protobuf-generated JavaScript Code
- Auto-generated JavaScript code from the same proto definition.
- Deserializes the serialized data from C# and directly calls JavaScript methods.
With WebView RPC, method calls between C# and JavaScript behave like regular function calls, significantly reducing the need for repetitive JSON parsing and bridge implementations. This structure becomes even more maintainable as the project scales.
Clone this whole repository.
Note
Require Viewplex Webview Asset(paid asset) to run sample.
# 1. Move to sample directory
cd webview_rpc_sample
# 2. Install dependencies
npm install
# 3. Build project
npm run build
- Install the
Protobuf
package via NuGet Package Manager. - Install the WebViewRpc package either via Package Manager or OpenUPM.
-
Add to
Packages/manifest.json
:{ "dependencies": { "com.kwanjoong.webviewrpc": "https://github.com/kwan3854/Unity-WebViewRpc.git?path=/Packages/WebviewRpc" } }
-
Or via Package Manager:
Window
→Package Manager
→Add package from git URL...
- Enter:
https://github.com/kwan3854/Unity-WebViewRpc.git?path=/Packages/WebviewRpc
-
Or via OpenUPM:
openupm add com.kwanjoong.webviewrpc
npm install [email protected]
import { VuplexBridge, WebViewRpcClient, WebViewRpcServer } from 'app-webview-rpc';
// RPC client
const bridge = new VuplexBridge();
const rpcClient = new WebViewRpcClient(bridge);
// RPC server
const rpcServer = new WebViewRpcServer(bridge);
Mac
brew install protobuf
protoc --version # Ensure compiler version is 3+
Windows
winget install protobuf
protoc --version # Ensure compiler version is 3+
Linux
apt install -y protobuf-compiler
protoc --version # Ensure compiler version is 3+
Download the latest release from the WebViewRPC Code Generator repository.
- Windows:
protoc-gen-webviewrpc.exe
- Mac:
protoc-gen-webviewrpc
- Linux: Not provided (requires manual build).
To handle environments with message size limitations, such as certain Android WebViews where the JavaScript bridge limit is around 1KB, WebView RPC includes a chunking feature. This allows large messages to be split into smaller chunks and reassembled on the receiving end.
When chunking is enabled, any message exceeding MaxChunkSize
is automatically divided into smaller parts. Each chunk is sent individually and then reassembled by the receiver. This process is handled transparently by the library.
The chunking system automatically accounts for:
- RPC Envelope Overhead: Approximately 150 bytes for metadata (requestId, method, chunkInfo)
- Base64 Encoding Overhead: About 34% increase in size
- Effective Payload Size: Actual usable data size after accounting for all overheads
WebView RPC provides several configuration options through WebViewRpcConfiguration
:
Control whether chunking is active. When disabled, large messages will be sent as-is, which may fail in environments with size limitations.
Unity (C#)
WebViewRpcConfiguration.EnableChunking = true; // Default: true
JavaScript
WebViewRpcConfiguration.enableChunking = true; // Default: true
Sets the maximum size of the final Base64-encoded message. The library automatically calculates the effective payload size after accounting for encoding overhead.
Unity (C#)
// Set max chunk size to 900 bytes (recommended for 1KB limit environments)
WebViewRpcConfiguration.MaxChunkSize = 900; // Default: 256KB
// Minimum: ~335 bytes (dynamically calculated)
// Maximum: 10MB
JavaScript
// Set max chunk size to 900 bytes
WebViewRpcConfiguration.maxChunkSize = 900; // Default: 256KB
Configure how long to wait for incomplete chunk sets before cleaning them up.
Unity (C#)
WebViewRpcConfiguration.ChunkTimeoutSeconds = 30; // Default: 30 seconds
// Minimum: 5 seconds
// Maximum: 300 seconds (5 minutes)
JavaScript
WebViewRpcConfiguration.chunkTimeoutSeconds = 30; // Default: 30 seconds
Limit the number of simultaneous chunked messages to prevent memory exhaustion.
Unity (C#)
WebViewRpcConfiguration.MaxConcurrentChunkSets = 100; // Default: 100
// Minimum: 10
// Maximum: 1000
JavaScript
WebViewRpcConfiguration.maxConcurrentChunkSets = 100; // Default: 100
WebView RPC provides utility methods to help you understand and optimize chunk sizing:
Unity (C#)
// Get the actual payload size available after overhead
int effectiveSize = WebViewRpcConfiguration.GetEffectivePayloadSize();
Console.WriteLine($"Effective payload size: {effectiveSize} bytes");
// Get the minimum safe chunk size
int minSize = WebViewRpcConfiguration.GetMinimumSafeChunkSize();
Console.WriteLine($"Minimum chunk size: {minSize} bytes");
// Validate current configuration
bool isValid = WebViewRpcConfiguration.IsChunkSizeValid();
if (!isValid)
{
Console.WriteLine("Warning: Chunk size too small for meaningful payload");
}
JavaScript
// Get the actual payload size available after overhead
const effectiveSize = WebViewRpcConfiguration.getEffectivePayloadSize();
console.log(`Effective payload size: ${effectiveSize} bytes`);
// Get the minimum safe chunk size
const minSize = WebViewRpcConfiguration.getMinimumSafeChunkSize();
console.log(`Minimum chunk size: ${minSize} bytes`);
// Validate current configuration
const isValid = WebViewRpcConfiguration.isChunkSizeValid();
if (!isValid) {
console.warn("Chunk size too small for meaningful payload");
}
- Size Configuration: For Android WebViews with 1KB limits, use
MaxChunkSize = 900
to safely stay under the limit - Consistent Settings: Ensure both Unity and JavaScript use identical configuration values
- Monitor Performance: If you see warnings about small effective payload sizes, consider increasing
MaxChunkSize
- Timeout Tuning: Adjust
ChunkTimeoutSeconds
based on your network conditions and message sizes - Testing: Test with your largest expected messages to ensure chunking works smoothly
Using the same configuration on both sides for bidirectional consistency:
Unity (C#)
void Start()
{
// Match the most restrictive environment (Android WebView 1KB)
WebViewRpcConfiguration.EnableChunking = true;
WebViewRpcConfiguration.MaxChunkSize = 900;
WebViewRpcConfiguration.ChunkTimeoutSeconds = 60;
WebViewRpcConfiguration.MaxConcurrentChunkSets = 50;
// Verify configuration
int effectiveSize = WebViewRpcConfiguration.GetEffectivePayloadSize();
Debug.Log($"Sending with effective payload: {effectiveSize} bytes per chunk");
// Initialize RPC
var bridge = new ViewplexWebViewBridge(webViewPrefab);
var server = new WebViewRPC.WebViewRpcServer(bridge);
}
JavaScript
// Match Unity settings for bidirectional consistency
WebViewRpcConfiguration.enableChunking = true;
WebViewRpcConfiguration.maxChunkSize = 900;
WebViewRpcConfiguration.chunkTimeoutSeconds = 60;
WebViewRpcConfiguration.maxConcurrentChunkSets = 50;
const effectiveSize = WebViewRpcConfiguration.getEffectivePayloadSize();
console.log(`Sending with effective payload: ${effectiveSize} bytes per chunk`);
const bridge = new VuplexBridge();
const rpcServer = new WebViewRpcServer(bridge);
Using different chunk sizes within the same platform constraints:
Android WebView Environment Example (1KB limit both directions)
// Unity (C#)
void Start()
{
WebViewRpcConfiguration.EnableChunking = true;
WebViewRpcConfiguration.MaxChunkSize = 990; // Unity uses 990-byte chunks
WebViewRpcConfiguration.ChunkTimeoutSeconds = 60;
WebViewRpcConfiguration.MaxConcurrentChunkSets = 50;
Debug.Log("Unity: Sending with 990-byte chunks, can receive any size");
}
// JavaScript
WebViewRpcConfiguration.enableChunking = true;
WebViewRpcConfiguration.maxChunkSize = 800; // JS uses 800-byte chunks
WebViewRpcConfiguration.chunkTimeoutSeconds = 60;
WebViewRpcConfiguration.maxConcurrentChunkSets = 50;
console.log("JS: Sending with 800-byte chunks, can receive any size");
Regular Browser Environment Example (large messages supported)
// Unity (C#)
WebViewRpcConfiguration.MaxChunkSize = 1024 * 1024; // 1MB chunks
// JavaScript
WebViewRpcConfiguration.maxChunkSize = 256 * 1024; // 256KB chunks
While each side can use different chunk sizes under the same platform constraints, there's little practical benefit to doing so.
Note
Chunking Configuration Independence and Platform Constraints
Platform/bridge constraints apply equally to both directions:
- Android WebView environment: Unity ↔ JavaScript both limited to ~1KB
- iOS WKWebView environment: Unity ↔ JavaScript both support larger messages
- Regular browser environment: Most support large messages
Technical Flexibility:
- Sender: Can set MaxChunkSize freely within platform constraints
- Receiver: Can receive and reassemble chunks of any size (MaxChunkSize doesn't affect reception)
Example - Android WebView Environment (1KB limit):
- Unity side: Sets MaxChunkSize = 990 bytes for sending
- JavaScript side: Sets MaxChunkSize = 800 bytes for sending
- Both sides receive and reassemble chunks from each other without issues
Recommendations:
- Required: Identify your platform/bridge message size limitations
- Optional Unification: Use identical settings for easier maintenance and debugging
- Receiver Settings: ChunkTimeoutSeconds and MaxConcurrentChunkSets affect receiver behavior
HelloWorld is a simple RPC service that receives a HelloRequest
message and returns a HelloResponse
message. In this example, we will implement HelloWorld and verify communication between the Unity client and the WebView client.
The HelloWorld service takes a HelloRequest
and returns a HelloResponse
. First, let's look at the example where the C# side acts as the server and the JavaScript side acts as the client.
- protobuf is used to define the request and response formats of the service.
- When the Unity client and the WebView have items to communicate, define the protobuf through discussion.
- The following example is the
HelloWorld.proto
file, definingHelloRequest
,HelloResponse
, and theHelloService
service. - In this example, the client side (JavaScript) calls the
SayHello
method, and the server side (C#) implements theSayHello
method to process the request and return a response.
syntax = "proto3";
package helloworld;
// (Can be used as the namespace when generated in C#)
option csharp_namespace = "HelloWorld";
// Request message
message HelloRequest {
string name = 1;
}
// Response message
message HelloResponse {
string greeting = 1;
}
// Simple example service
service HelloService {
// [one-way] Request -> Response
rpc SayHello (HelloRequest) returns (HelloResponse);
}
- We use the
protoc
compiler to convert the protobuf file into C# and JavaScript. - The
protoc
compiler transforms protobuf files into C# and JavaScript. - A customized code generator for WebView RPC is also available.
- Run the following commands to generate C# and JavaScript code from the protobuf file.
protoc -I. --csharp_out=. HelloWorld.proto
// This produces HelloWorld.cs.
protoc \
--plugin=protoc-gen-webviewrpc=./protoc-gen-webviewrpc \
--webviewrpc_out=cs_server:. \
-I. HelloWorld.proto
// This produces HelloWorld_HelloServiceBase.cs.
Important
npx pbjs HelloWorld.proto --es6 hello_world.js
// This produces hello_world.js.
// Recommend setting the output filename to the same name as the service defined in the protobuf file.
protoc \
--plugin=protoc-gen-webviewrpc=./protoc-gen-webviewrpc \
--webviewrpc_out=js_client:. \
-I. HelloWorld.proto
// This produces HelloWorld_HelloServiceClient.js.
- Add the generated code to each respective project.
- You can use a GitHub action so that code is automatically generated and added to your project.
- The bridge code mediates communication between C# and JavaScript.
- WebViewRpc is abstracted so it can be used with any WebView library.
- Implement the bridge code according to your chosen WebView library.
- Below is an example using Viewplex's CanvasWebViewPrefab.
using System;
using Vuplex.WebView;
using WebViewRPC;
public class ViewplexWebViewBridge : IWebViewBridge
{
public event Action<string> OnMessageReceived;
private readonly CanvasWebViewPrefab _webViewPrefab;
public ViewplexWebViewBridge(CanvasWebViewPrefab webViewPrefab)
{
_webViewPrefab = webViewPrefab;
_webViewPrefab.WebView.MessageEmitted += (sender, args) =>
{
OnMessageReceived?.Invoke(args.Value);
};
}
public void SendMessageToWeb(string message)
{
_webViewPrefab.WebView.PostMessage(message);
}
}
export class VuplexBridge {
constructor() {
this._onMessageCallback = null;
this._isVuplexReady = false;
this._pendingMessages = [];
// 1) If window.vuplex already exists, use it immediately
if (window.vuplex) {
this._isVuplexReady = true;
} else {
// Otherwise, wait for the 'vuplexready' event
window.addEventListener('vuplexready', () => {
this._isVuplexReady = true;
// Send all pending messages
for (const msg of this._pendingMessages) {
window.vuplex.postMessage(msg);
}
this._pendingMessages = [];
});
}
// 2) C# -> JS messages: "vuplexmessage" event
// event.value contains the string (sent by C# PostMessage)
window.addEventListener('vuplexmessage', event => {
const base64Str = event.value; // Typically Base64
if (this._onMessageCallback) {
this._onMessageCallback(base64Str);
}
});
}
/**
* JS -> C#: sends string (base64Str)
*/
sendMessage(base64Str) {
// Vuplex serializes JS objects to JSON,
// but if we pass a string, it sends the string as is.
if (this._isVuplexReady && window.vuplex) {
window.vuplex.postMessage(base64Str);
} else {
// If vuplex isn't ready yet, store messages in a queue
this._pendingMessages.push(base64Str);
}
}
/**
* onMessage(cb): registers a callback to receive strings from C#
*/
onMessage(cb) {
this._onMessageCallback = cb;
}
}
public class WebViewRpcTester : MonoBehaviour
{
[SerializeField] private CanvasWebViewPrefab webViewPrefab;
private async void Start()
{
await InitializeWebView(webViewPrefab);
// Create the bridge
var bridge = new ViewplexWebViewBridge(webViewPrefab);
// Create the server
var server = new WebViewRPC.WebViewRpcServer(bridge)
{
Services =
{
// Bind HelloService
HelloService.BindService(new HelloWorldService()),
// Add other services if necessary
}
};
// Start the server
server.Start();
}
private async Task InitializeWebView(CanvasWebViewPrefab webView)
{
// Example uses Viewplex's CanvasWebViewPrefab
await webView.WaitUntilInitialized();
webView.WebView.LoadUrl("http://localhost:8081");
await webView.WebView.WaitForNextPageLoadToFinish();
}
}
using Cysharp.Threading.Tasks;
using HelloWorld;
using UnityEngine;
namespace SampleRpc
{
// Inherit HelloServiceBase and implement the SayHello method.
// HelloServiceBase is generated from HelloWorld.proto.
public class HelloWorldService : HelloServiceBase
{
public override async UniTask<HelloResponse> SayHello(HelloRequest request)
{
Debug.Log($"Received request: {request.Name}");
// Example async operation
await UniTask.Delay(100);
return new HelloResponse()
{
// Process the request and return a response
Greeting = $"Hello, {request.Name}!"
};
}
}
}
// 1) Create a bridge
const bridge = new VuplexBridge();
// 2) Create an RpcClient
const rpcClient = new WebViewRpcClient(bridge);
// 3) Create a HelloServiceClient
const helloClient = new HelloServiceClient(rpcClient);
document.getElementById('btnSayHello').addEventListener('click', async () => {
try {
const reqObj = { name: "Hello World! From WebView" };
console.log("Request to Unity: ", reqObj);
const resp = await helloClient.SayHello(reqObj);
console.log("Response from Unity: ", resp.greeting);
} catch (err) {
console.error("Error: ", err);
}
});
- Run the
WebViewRpcTester
script in Unity, and open the WebView. - When you click the button in the WebView, Unity processes the request via
HelloService
and returns a response.
- The reverse scenario can be implemented in the same way.
- Since the common code is already generated, generate C# client code and JavaScript server code.
protoc \
--plugin=protoc-gen-webviewrpc=./protoc-gen-webviewrpc \
--webviewrpc_out=cs_client:. \
-I. HelloWorld.proto
protoc \
--plugin=protoc-gen-webviewrpc=./protoc-gen-webviewrpc \
--webviewrpc_out=js_server:. \
-I. HelloWorld.proto
public class WebViewRpcTester : MonoBehaviour
{
[SerializeField] private CanvasWebViewPrefab webViewPrefab;
private void Awake()
{
Web.ClearAllData();
}
private async void Start()
{
await InitializeWebView(webViewPrefab);
// Create the bridge
var bridge = new ViewplexWebViewBridge(webViewPrefab);
// Create an RpcClient
var rpcClient = new WebViewRPC.WebViewRpcClient(bridge);
// Create a HelloServiceClient
var client = new HelloServiceClient(rpcClient);
// Send a request
var response = await client.SayHello(new HelloRequest()
{
Name = "World"
});
// Check the response
Debug.Log($"Received response: {response.Greeting}");
}
private async Task InitializeWebView(CanvasWebViewPrefab webView)
{
await webView.WaitUntilInitialized();
webView.WebView.LoadUrl("http://localhost:8081");
await webView.WebView.WaitForNextPageLoadToFinish();
}
}
// 1) Create a bridge
const bridge = new VuplexBridge();
// 2) Create an RpcServer
const rpcServer = new WebViewRpcServer(bridge);
// 3) Create a service implementation
const impl = new MyHelloServiceImpl();
// 4) Bind the service
const def = HelloService.bindService(impl);
// 5) Register the service
rpcServer.services.push(def);
// 6) Start the server
rpcServer.start();
import { HelloServiceBase } from "./HelloWorld_HelloServiceBase.js";
// Inherit HelloServiceBase from the auto-generated HelloWorld_HelloServiceBase.js
export class MyHelloServiceImpl extends HelloServiceBase {
async SayHello(requestObj) {
// Check the incoming request
console.log("JS Server received: ", requestObj);
// Example async operation
await new Promise(resolve => setTimeout(resolve, 100));
// Process the request and return a response
return {
greeting: "Hello from JS! I got your message: " + requestObj.name
};
}
}
This project is licensed under the MIT License - see the LICENSE file for details.