SkillAgentSearch skills...

Autowire

Macros for simple/safe RPCs between Scala applications, including ScalaJS/ScalaJVM

Install / Use

/learn @lihaoyi/Autowire
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Autowire 0.3.3

Scala.js

Autowire is a pair of macros that allows you to perform type-safe, reflection-free RPC between Scala systems. Autowire allows you to write type-safe Ajax/RPC calls that look like:

// shared interface
trait Api{
  def add(x: Int, y: Int, z: Int): Int 
}

// server-side router and implementation 
object Server extends autowire.Server...
object ApiImpl extends Api
  def add(x: Int, y: Int, z: Int) = x + y + z 
}

// client-side callsite

import autowire._ // needed for correct macro application

object Client extends autowire.Client...
Client[Api].add(1, 2, 3).call(): Future[Int]
//         |    |             |
//         |    |             The T is pickled and wrapped in a Future[T]
//         |    The arguments to that method are pickled automatically
//         Call a method on the `Api` trait

This provides a range of nice properties: under-the-hood the macro converts your method calls into RPCs, together with the relevant serialization code, but you do not need to deal with that yourself. Furthermore, since the RPCs appear to be method calls, tools like IDEs are able to work with them, e.g. doing project-wide renames or analysis (jump-to-definition, find-usages, etc.).

Most importantly, the compiler is able to typecheck these RPCs, and ensure that you are always calling them with the correct arguments, and handling the return-value correctly in turn. This removes an entire class of errors.

Autowire is completely agnostic to both the serialization library, and the transport-mechanism.

Getting Started

Autowire is available at the following maven coordinates:

"com.lihaoyi" %% "autowire" % "0.3.3" // for Scala-JVM
"com.lihaoyi" %%% "autowire" % "0.3.3" // Scala.js (1.4.0+) and Scala Native (0.4.0)

This 0.3.3 can be used by publishing locally:

git clone https://github.com/lihaoyi/autowire.git
cd autowire
./mill __.publishLocal

And then add a library dependency in your build.sbt for your shared project:

libraryDependencies ++= Seq(
  "com.lihaoyi" %%% "autowire" % "0.3.3",
  ...
)

Autowire 0.3.3 works for Scala.js 1.x and both Scala 2.12 and 2.13 For Scala.js 0.6.x users, please use autowire 0.2.6.

Autowire works on both Scala-JVM and Scala-JS, meaning you can use it to get type-safe Ajax calls between a browser and your servers.

Minimal Example

Here is a minimal example of Autowire in action, using uPickle to serialize the transferred data:

import autowire._
import upickle._

// shared API interface 
trait MyApi{
  def doThing(i: Int, s: String): Seq[String]
}

// server-side implementation, and router 
object MyApiImpl extends MyApi{
  def doThing(i: Int, s: String) = Seq.fill(i)(s)
}
object MyServer extends autowire.Server[String, upickle.Reader, upickle.Writer]{
  def write[Result: Writer](r: Result) = upickle.write(r)
  def read[Result: Reader](p: String) = upickle.read[Result](p)
  
  val routes = MyServer.route[MyApi](MyApiImpl)
}

// client-side implementation, and call-site
object MyClient extends autowire.Client[String, upickle.Reader, upickle.Writer]{
  def write[Result: Writer](r: Result) = upickle.write(r)
  def read[Result: Reader](p: String) = upickle.read[Result](p)

  override def doCall(req: Request) = {
    println(req)
    MyServer.routes.apply(req)
  }
}

MyClient[MyApi].doThing(3, "lol").call().foreach(println)
// List(lol, lol, lol)

In this example, we have a shared MyApi trait, which contains a trait/interface which is shared by both client and server. The server contains an implementation of this trait, while the client can make calls against the trait using the route macro provided by MyServer.

Although in this example everything is happening locally, this example goes through the entire serialization/de-serialization process when transferring the arguments/return-value. Thus, it is trivial to replace the direct call to MyServer.routes to a remote call over HTTP or TCP or some other transport with MyClient and MyServer/MyApiImpl living on different machines or even running on different platforms (e.g. Scala-JS/Scala-JVM).

Here's a longer example, which takes advantage of autowire's cross-platform-ness to write an interactive client-server web-app.

Note that:

  • The client proxy, MyClient[MyApi] in this example, must be explicitly written out, and followed by .call() at each site where a remote call is made. That is, you cannot assign MyClient[MyApi] to a variable and use that instead.
  • The methods in the autowired API can also be asynchronous, in which case they return Future[T]. So the example above could be modeled async as def doThing(i: Int, s: String): Future[Seq[String]].

Reference

This section goes deeper into the steps necessary to use Autowire, and should cover extension points not obvious from the above example.

To begin with, the developer has to implement two interfaces, Client:

trait Client[PickleType, Reader[_], Writer[_]] {
  type Request = Core.Request[PickleType]
  def read[Result: Reader](p: PickleType): Result
  def write[Result: Writer](r: Result): PickleType
  
  /**
   * A method for you to override, that actually performs the heavy
   * lifting to transmit the marshalled function call from the [[Client]]
   * all the way to the [[Core.Router]]
   */
  def doCall(req: Request): Future[PickleType]
}

And Server

trait Server[PickleType, Reader[_], Writer[_]] {
  type Request = Core.Request[PickleType]
  type Router = Core.Router[PickleType]
  
  def read[Result: Reader](p: PickleType): Result
  def write[Result: Writer](r: Result): PickleType

  /**
   * A macro that generates a `Router` PartialFunction which will dispatch incoming
   * [[Requests]] to the relevant method on [[Trait]]
   */
  def route[Trait](target: Trait): Router = macro Macros.routeMacro[Trait, PickleType]
}

Both Client and Server are flexible: you get to specify multiple type parameters:

  • PickleType: or what type you want to serialize your arguments/return-value into. Typically something like String, Array[Byte], ByteString, etc.
  • Reader[_]: a context-bound used to convert data from PickleType to a desired type T. Typically something like upickle.Reader, play.api.libs.json.Reads, etc.
  • Writer[_]: a context-bound used to convert data to PickleType from an original type T. Typically something like upickle.Writer, play.api.libs.json.Writes, etc.

If the serialization library you're using doesn't need Reader[_] or Writer[_] context-bound, you can use autowire.Bounds.None as the context-bound, and it will be filled in automatically. You then need to override the read and write methods of both sides, in order to tell Autowire how to serialize and deserialize your typed values.

Lastly, you need to wire together client and server to get them talking to one another: this is done by overriding Client.doCall, which takes the Request object holding the serialized arguments and returns a Future containing the serialized response, and by using Server.route, which generates a Router PartialFunction which you can then feed Request object into to receive a serialized response in return. These types are defined as

object Core {
  /**
   * The type returned by the [[Server.route]] macro; aliased for
   * convenience, but it's really just a normal `PartialFunction`
   * and can be combined/queried/treated like any other.
   */
  type Router[PickleType] = PartialFunction[Request[PickleType], Future[PickleType]]

  /**
   * A marshalled autowire'd function call.
   *
   * @param path A series of path segments which illustrate which method
   *             to call, typically the fully qualified path of the
   *             enclosing trait followed by the name of the method
   * @param args Serialized arguments for the method that was called. Kept
   *             as a Map of arg-name -> serialized value. Values which
   *             exactly match the default value are omitted, and are
   *             simply re-constituted by the receiver.
   */
  case class Request[PickleType](path: Seq[String], args: Map[String, PickleType])
}

is outside the scope of Autowire, as is how you serialize/deserialize the arguments/return-value using the read and write functions.

In short,

  • {Client, Server}.{read, write} is your chance to substitute in whatever serialization library you want. Any library should work, as long as you can put the serialization code into the read and write functions to serialize an object of type T. Autowire works with any transport and any serialization library, including Java-serialization, Kryo and Play-Json, with unit tests to ensure that they are working.
  • Client.doCall and Server.route is your chance to choose whatever transport mechanism you want to use. By that point, the arguments/return-value have already been mostly serialized, with only a small amount of structure (e.g. the map of argument-names to serialized-values) left behind. Exactly how you get the Request object from the Client to the Server (HTTP, TCP, etc.) and the response-data back is up to you as long as you can give Autowire a Future[PickleType] in exchange for its Request.

If you still don't fully understand, and need help getting something working, take a look at the complete example.

Related Skills

View on GitHub
GitHub Stars381
CategoryDevelopment
Updated4d ago
Forks47

Languages

Scala

Security Score

80/100

Audited on Apr 3, 2026

No findings