Search code examples
scaladynamicproxymacrosintercept

Generating a dynamic proxy that intercepts all methods of an object in Scala using macros


I have several objects with different methods (without any traits/interfaces) and I'd like to intercept those methods in order to add them some kind of a debug feature. I think I can achieve that with macros in Scala 2.11.12. Macros seem to be quite powerful but not trivial at all.

The methods that I want to get intercepted can call among them, that's why I can't use a simple Proxy approach. I've also tried using AspectJ, but don't want to add any dependencies to my project if I can help it.

This is what I'm trying, but I don't know how to call the methods I'm trying to intercept.

import scala.language.experimental.macros
import scala.reflect.macros.whitebox

object MacroProxy {
  def createProxy[T](original: T): T = macro createProxyImpl[T]

  def createProxyImpl[T: c.WeakTypeTag](c: whitebox.Context)(original: c.Expr[T]): c.Expr[T] = {
    import c.universe._
    val className = original.getClass.getSimpleName

    val m = weakTypeOf[T].members.iterator.toList
      .filter(m => m.isMethod).map(_.asMethod).map { m =>
        q"""override def  ${m.name} = $m"""
    }

    c.Expr {
      q"""new $className {  ..$m } """
    }
  }
}

I'm getting this error

missing argument list for method mymethod in class MyClass Unapplied methods are only converted to functions when a function type is expected. You can make this conversion explicit by writing mymethod _ or mymethod(_) instead of mymethod.


Solution

  • Several things:

    1. nothing indicates that you need whitebox macros since the type is known from the beginning
    2. what you do sounds weirdly similar to what ScalaMock does, so borrowing implementation from it might be easier than implementing something yourself
    3. what you did is equal to :
      class Foo {
        def a(): Int = ...
        def b(a: String): Double = ...
      }
      new Foo {
        override def a = original.a
        override def b = original.b
      }
      
      You basically skipped on argument lists and compiler thinks you are trying to turn methods into functions through eta expansion (which wouldn't let you override). You'd have to do something like:
      val tpe = weakTypeOf[T]
      tpe.members.iterator.toList
       .filter(m => m.isMethod).map(_.asMethod).map { m =>
           val name = m.name
           val params = m.typeSignatureIn(tpe).paramLists
           val arguments = params.map(_.map(_.name.toTermName))
           q"""override def $name($...params) = $original.$name(...$arguments)"""
       }
      
      Though it would be just the beginning:
      • to be able to call new $className { ... } you have to check if class doesn't require some constructor with parameters
      • you have to pay attention when to use .. (to expand sequence of definitions/parameters/trees) and ... (to expand sequence of sequences, which appear with every parameter list)
      • you'd have to check which symbols are public and which aren't
      • you'd have to remember about manually applying types, because type parameters aren't magically propagating themselves
      • you'd have to repeat all of that in Scala 3 since it has a completely new macro API and everything you would do here would be DOA in Scala 3

    Basically, what you are trying to do is doable, but unless you want to support only most basic cases, this would be much more work than a single afternoon.