Multiple RPC Handlers in Module

How would I go about adding multiple RPC handlers to the gateway? The current module I’m working on has a fairly large API and I would like to split it over organize it over multiple classes. Do I have to do some sort of switch statement in the getRPCHandler class of the GatewayHook? I see that the ModuleRPCFactory allows you to request separate interfaces.

You can put your implementations anywhere you want them. The actual RPC implementation returned from the gateway hook can be full of one-liners that delegate to the actual class and method that is appropriate.

The RPC engine does the first “switch” for you with the method name. Nothing prevents you from defining a second level of naming for some of the first level calls.

But I’d have to setup a ton of 1 line methods to do this?

So just to clarify what I’m trying to do - I have a bunch of data classes and service classes doing CRUD operations on the gateway representing some objects in my module and I was hoping I could make a abstract RPC handler like this:

public abstract class RpcHandler<T> {}...

Then I’d like to find a way to have a top level RPC handler class return a reference to that so I can save myself from having to write 1000’s of one liners delegating everything to the correct member service method.

I think the best you’re going to get is the “delegate” pattern.

Imagine you have interfaces IFoo and IBar, and class ModuleRPC.

Implement ModuleRPC like this:

public class ModuleRPC implements IFoo, IBar {

    IFoo fooImpl;
    IBar barImpl;

    @Override
    public void bar() {
        barImpl.bar();
    }

    @Override
    public void foo() {
        fooImpl.foo();
    }

}

but let IntelliJ generate all the delegated methods for you once you’ve added the impl references via Code > Generate... > Delegate Methods....

1 Like

And if you’re allergic to boilerplate, “just” switch to Kotlin :wink:

class ModuleRPC(context: GatewayContext) : IFoo by FooImpl(context), IBar by BarImpl(context)

Strictly speaking, the RPC system just does switching on string identifiers. First is the module ID, where Ignition looks up the module to get your handler. It then looks up the method name (second string identifier) via reflection and hands off the rest of the arguments to that method.

Nothing prevents you from setting up another layer of indirection, into classes of your choice. Or some mixture, where some methods do further indirection and some perform a direct action.

You don't even have to make interfaces for everything if you simply define methods that accept Object... variants.

This is what I ended up doing actually. I thought I’d post the solution here if anybody is looking for the same thing here in the future.

I made a hash-map of the various RPC service I would like to be exposed then defined a single method in my gateway RPC handler:

    @Override
    public <T> T invoke(Class<?> apiClass, String method, Object... args) {
        var service = serviceMap.get(apiClass);
        Objects.requireNonNull(service, "No service found for class " + apiClass);
        try {
            return ReflectionUtils.invokeMethod(service, method, args);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

Then added a method to a reflections utility class to call the method I want:

    @SuppressWarnings("unchecked")
    public static <T> T invokeMethod(Object bean, String methodName, Object... args) throws IllegalAccessException, 
            InvocationTargetException, NoSuchMethodException {
        
        Class<?> beanClass = bean.getClass();
        Class<?>[] argTypes = new Class<?>[args.length];
        for (int i = 0; i < args.length; i++) {
            argTypes[i] = args[i].getClass();
        }

        Method method = beanClass.getMethod(methodName, argTypes);
        
        return (T) method.invoke(bean, args);
    }

I quite liked having a common interface so that I have to fulfill a contract on both ends of the RPC call because I think that will prevent a lot of bugs…

3 Likes

Hello @cody.tamaki,

I'm trying to reproduce the same RPC Handler as you.
But I don't understand how you override the invoke method.

Does your RPC Handler extends/implements from another class?

I'm also trying to use multiple RPC Handlers, but can't figure it out even after reading through this Post a hundred times.

Here is what I've got so far.
In the Common scope:
image

In the Gateway scope:
image

In the Client Scope:
image

Under ClientTagHandler I want to have a rpc call that calls the methods defined under GatewayTagHandler. And under ClientLV_Scripts I want to have a rpc call that calls the methods defined under GatewayLV_Scripts.

What do I need to do to make this work?

You can't. A module can only have one RPC handler.

Good news: You will be able to have arbitrarily many RPC handlers in 8.3, under custom "namespaces" within your module.

2 Likes

Any word on when 8.3 is coming out?

No earlier than the end of this year.

A beta release may be available around ICC, in September.

Are there still plans to provide early access for module authors once API changes are finalized?

The beta will be the early access for most developers.

1 Like

Here is what I've done now.
GatewayHook:

@Override
public Object getRPCHandler(ClientReqSession session, String projectName) {
    
    return new RPCHandler();
}

RPCHandler:

public class RPCHandler implements RPCHandlerIntf {
    
    @Override
    public TagHandler getTagHandler(){
        return new GatewayTagHandler();
    }
    
    @Override
    public ScriptInterface getScripts(){
        return new GatewayScripts();
    }
    
    @Override
    public double square(double value){
        return value*value;
    }
}

RPCHandlerIntf:

public interface RPCHandlerIntf {
    
    public TagHandler getTagHandler();
    
    public ScriptInterface getScripts();
    
    public double square(double value);
}

ClientScripts:

public class ClientScripts extends AbstractScripts {
    
    public static RPCHandlerIntf rpc = ModuleRPCFactory.create(
            "com.lvcontrol.ignition",
            RPCHandlerIntf.class);
    
    public ClientScripts(){
        
    }
    
    @Override
    public double multiply(double val1, double val2) {
        return rpc.getScripts().multiply(val1, val2);
    }
    
    public double square(double value){
        return rpc.square(value);
    }
}

ClientHook:

public void initializeScriptManager(ScriptManager manager) {
        super.initializeScriptManager(manager);

        manager.addScriptModule("system.lvcontrol",
            new ClientScripts(),
            new PropertiesFileDocProvider()
        );
    }

The square(value) script works, but the multiply(val1,val2) script doesn't.

The multiply script throws the following, anyone know why?

>>> 
Java Traceback:
Traceback (most recent call last):
  File "<input>", line 1, in <module>
	at jdk.proxy2/jdk.proxy2.$Proxy51.getScripts(Unknown Source)
	at com.lvcontrol.ignition.scripting.ClientScripts.multiply(ClientScripts.java:28)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.base/java.lang.reflect.Method.invoke(Unknown Source)
java.lang.reflect.UndeclaredThrowableException: java.lang.reflect.UndeclaredThrowableException
	at org.python.core.Py.JavaError(Py.java:545)
	at org.python.core.Py.JavaError(Py.java:536)
	at org.python.core.PyReflectedFunction.__call__(PyReflectedFunction.java:192)
	at com.inductiveautomation.ignition.common.script.ScriptManager$ReflectedInstanceFunction.__call__(ScriptManager.java:553)
	at org.python.core.PyObject.__call__(PyObject.java:477)
	at org.python.core.PyObject.__call__(PyObject.java:481)
	at org.python.pycode._pyx10.f$0(<input>:1)
	at org.python.pycode._pyx10.call_function(<input>)
	at org.python.core.PyTableCode.call(PyTableCode.java:173)
	at org.python.core.PyCode.call(PyCode.java:18)
	at org.python.core.Py.runCode(Py.java:1703)
	at org.python.core.Py.exec(Py.java:1747)
	at org.python.util.PythonInterpreter.exec(PythonInterpreter.java:277)
	at org.python.util.InteractiveInterpreter.runcode(InteractiveInterpreter.java:130)
	at com.inductiveautomation.ignition.designer.gui.tools.jythonconsole.JythonConsole$ConsoleWorker.doInBackground(JythonConsole.java:628)
	at com.inductiveautomation.ignition.designer.gui.tools.jythonconsole.JythonConsole$ConsoleWorker.doInBackground(JythonConsole.java:616)
	at java.desktop/javax.swing.SwingWorker$1.call(Unknown Source)
	at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
	at java.desktop/javax.swing.SwingWorker.run(Unknown Source)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
	at java.base/java.lang.Thread.run(Unknown Source)
Caused by: java.lang.reflect.UndeclaredThrowableException
	at jdk.proxy2/jdk.proxy2.$Proxy51.getScripts(Unknown Source)
	at com.lvcontrol.ignition.scripting.ClientScripts.multiply(ClientScripts.java:28)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.base/java.lang.reflect.Method.invoke(Unknown Source)
	at org.python.core.PyReflectedFunction.__call__(PyReflectedFunction.java:190)
	... 19 more
Caused by: com.inductiveautomation.ignition.client.gateway_interface.GatewayException: class com.lvcontrol.ignition.scripting.GatewayScripts cannot be cast to class java.io.Serializable (com.lvcontrol.ignition.scripting.GatewayScripts is in unnamed module of loader com.inductiveautomation.ignition.gateway.modules.ModuleClassLoader @430e86c0; java.io.Serializable is in module java.base of loader 'bootstrap')
	at com.inductiveautomation.ignition.client.gateway_interface.GatewayInterface.newGatewayException(GatewayInterface.java:360)
	at com.inductiveautomation.ignition.client.gateway_interface.GatewayInterface.sendMessage(GatewayInterface.java:334)
	at com.inductiveautomation.ignition.client.gateway_interface.GatewayInterface.sendMessage(GatewayInterface.java:287)
	at com.inductiveautomation.ignition.client.gateway_interface.GatewayInterface.moduleInvokeSafe(GatewayInterface.java:930)
	at com.inductiveautomation.ignition.client.gateway_interface.ModuleRPCFactory$DynamicRPCHandler.invoke(ModuleRPCFactory.java:53)
	... 26 more
Caused by: java.lang.ClassCastException: class com.lvcontrol.ignition.scripting.GatewayScripts cannot be cast to class java.io.Serializable (com.lvcontrol.ignition.scripting.GatewayScripts is in unnamed module of loader com.inductiveautomation.ignition.gateway.modules.ModuleClassLoader @430e86c0; java.io.Serializable is in module java.base of loader 'bootstrap')
	at com.inductiveautomation.ignition.gateway.servlets.gateway.functions.ModuleInvoke.invoke(ModuleInvoke.java:172)
	at com.inductiveautomation.ignition.gateway.servlets.Gateway.doPost(Gateway.java:435)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:523)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:590)
	at com.inductiveautomation.ignition.gateway.bootstrap.MapServlet.service(MapServlet.java:86)
	at org.eclipse.jetty.servlet.ServletHolder$NotAsync.service(ServletHolder.java:1410)
	at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:764)
	at org.eclipse.jetty.servlet.ServletHandler$ChainEnd.doFilter(ServletHandler.java:1665)
	at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:527)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:131)
	at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:578)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:223)
	at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1570)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:221)
	at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1384)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:176)
	at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:484)
	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1543)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:174)
	at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1306)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:129)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)
	at com.inductiveautomation.catapult.handlers.RemoteHostNameLookupHandler.handle(RemoteHostNameLookupHandler.java:121)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)
	at org.eclipse.jetty.rewrite.handler.RewriteHandler.handle(RewriteHandler.java:301)
	at org.eclipse.jetty.server.handler.HandlerList.handle(HandlerList.java:51)
	at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:141)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)
	at org.eclipse.jetty.server.Server.handle(Server.java:563)
	at org.eclipse.jetty.server.HttpChannel.lambda$handle$0(HttpChannel.java:505)
	at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:762)
	at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:497)
	at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:282)
	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:314)
	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:100)
	at org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.runTask(AdaptiveExecutionStrategy.java:416)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.consumeTask(AdaptiveExecutionStrategy.java:385)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.tryProduce(AdaptiveExecutionStrategy.java:272)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.lambda$new$0(AdaptiveExecutionStrategy.java:140)
	at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:411)
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:969)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1194)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1149)
	at java.lang.Thread.run(null)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
	at jdk.proxy2/jdk.proxy2.$Proxy51.getScripts(Unknown Source)
	at com.lvcontrol.ignition.scripting.ClientScripts.multiply(ClientScripts.java:28)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.base/java.lang.reflect.Method.invoke(Unknown Source)
java.lang.reflect.UndeclaredThrowableException: java.lang.reflect.UndeclaredThrowableException
>>> 

The RPC proxy stuff is trying to return an instance of your GatewayScripts class to the client, because of this line in ClientScripts:
rpc.getScripts().multiply(val1, val2)