Welcome! Please see the About page for a little more info on how this works.

+2 votes
in Compiler by

As shown in the following example, the protocol method, pfoo doesn't appear to be directly linked.

$ clj -J-Dclojure.compiler.direct-linking=true
Clojure 1.10.1
user=> (defprotocol Foo (pfoo [this]))
user=> (defn libfn [x] (pfoo x))
user=> (extend-protocol Foo String (pfoo [_] :dude))
user=> (libfn "hello")
user=> (alter-var-root #'user/pfoo (fn [old] (fn [this] (prn :this) (old this))))
#object[user$eval178$fn__179$fn__180 0x18eec010 "user$eval178$fn__179$fn__180@18eec010"]
user=> (libfn "hello")

The reason I ask is because I'm trying to find a generic way to patch protocol methods to work within sci + graal native-image. The constraints are:

  • works under direct-linking (important for native-image use)
  • use with 3rd party libraries
    • avoid source code changes to target protocols (can't add ^:dynamic, ^:redef meta)

I have a solution that works here, but it relies on the fact that protocol vars can be altered using alter-var-root at compile time even under direct linking. Is that a safe assumption?

2 Answers

+1 vote
selected by
Best answer

the way protocol functions are created currently they will not be compiled with a invokeStatic method (they close over some data), so the compiler will never compile them as a call to the invokeStatic static method (which is what direct linking does).

It seems very unlikely that this would change, but guarantee is a strong word.

Something to keep in mind is whenever a protocol definition is re-evaluated the vars that point to the functions are reset.

0 votes
edited by

There are other implementation details to deal with (as of Clojure 1.10.3), and I would be prepared to update your implementation as Clojure changes (ie., everything I mention below relies on completely undocumented implementation details relevant to Clojure 1.8-1.11):
1. As mentioned by hiredman, protocol methods are reset by extend. Here is the related issue. If you want your protocol monkey patch to survive an extend, you need to alter the :method-builders value for that protocol. However, there are subtle requirements for propagating .__methodImplCache to the actual implementation. From a glance, I don't think your code handles this correctly (the inner cache is never updated).
2. Protocol methods are inlined in some cases (end users only really notice this when you see a No single method ... error when you mess up a protocol call). This means your call (pfoo x) becomes (.pfoo x) and your monkey patch is circumvented. Try adding :inline metadata to the protocol method to avoid this:

;; untested
(-> #'pfoo alter-meta assoc :inline (fn [& args] `((do pfoo) ~@args))

Note that the inlining must be enforced before the Compiler sees any invocations--once it's inlined, it's too late.

I may release a library to help library authors do this kind of thing.