coroutines icon indicating copy to clipboard operation
coroutines copied to clipboard

Clojure support

Open darkleaf opened this issue 5 years ago • 4 comments

H! I've been trying to use coroutines for Clojure. I found that your library is promising. But Clojure's compiler makes slightly different bytecode than Java does.

(defn x []
  (let [my-coroutine (reify Coroutine
                       (run [_ c]
                         (.suspend c)))
        r            (CoroutineRunner. my-coroutine)]
    (doto r
      (.execute))))

The compiler produces this bytecode

  public void run(com.offbynull.coroutines.user.Continuation) throws java.lang.Exception;
    Code:
       0: aload_1
       1: aconst_null
       2: astore_1
       3: checkcast     #30                 // class com/offbynull/coroutines/user/Continuation
       6: invokevirtual #33                 // Method com/offbynull/coroutines/user/Continuation.suspend:()V
       9: aconst_null
      10: pop
      11: return

Java Decompiler decompiles it to the following Java code

 public void run(Continuation c) throws Exception {
    c = null; // wrong decompilation
    c.suspend();
    null;
  }

I think that the decompiler is wrong because every Java method call in Clojure has the same bytecode. So the bytecode is correct.

But when I run the code I've got a NullPointerException on the (.suspend c) line. Maybe this library has the same mistake?

darkleaf avatar Aug 29 '20 15:08 darkleaf

Hi,

I generally don't have the time to maintain this project anymore, but I may be able to take a look if you can attach the class file both before and after instrumentation.

I suspect that the clojure compiler doesn't maintain structure between source code and compiled class the same way that the Java compiler does. Some other JVM languages are the same way. If that's the case, this library won't work. Your best bet would be to just extend the language itself to bake in coroutines.

On Sat, Aug 29, 2020, 8:49 AM Mikhail Kuzmin, [email protected] wrote:

H! I've been trying to use coroutines for Clojure. I found that your library is promising. But Clojure's compiler makes slightly different bytecode than Java does.

(defn x [] (let [my-coroutine (reify Coroutine (run [_ c] (.suspend c))) r (CoroutineRunner. my-coroutine)] (doto r (.execute))))

The compiler produces this bytecode

public void run(com.offbynull.coroutines.user.Continuation) throws java.lang.Exception; Code: 0: aload_1 1: aconst_null 2: astore_1 3: checkcast #30 // class com/offbynull/coroutines/user/Continuation 6: invokevirtual #33 // Method com/offbynull/coroutines/user/Continuation.suspend:()V 9: aconst_null 10: pop 11: return

Java Decompiler http://java-decompiler.github.io/ decompiles it to the following Java code

public void run(Continuation c) throws Exception { c = null; // wrong decompilation c.suspend(); null; }

I think that the decompiler is wrong because every Java method call in Clojure has the same bytecode. So the bytecode is correct.

But when I run the code I've got a NullPointerException on the (.suspend c) line. Maybe this library has the same mistake?

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/offbynull/coroutines/issues/92, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABQPQIW54Y6Q772ORJIBTD3SDEPR3ANCNFSM4QPB4EOA .

offbynull avatar Aug 29 '20 16:08 offbynull

It's the class before the transformation. core$x$reify__12279.class.zip

I use the Java agent, so I don't have a transformed class file. If you tell me how I can save transformed class I would send it.

darkleaf avatar Aug 29 '20 16:08 darkleaf

In the class you posted, I don't see the CoroutineRunner.execute() invocation anywhere? where is it?

Also, do you have a stacktrace for the NPE?

CoroutineRunner creates the Continuation object, which gets passed to the run method in the class you posted. The byte code transformations should remove the call to suspend() and inject a bunch of code that messes around with the Continuation object as well as the control flow of your run method.

offbynull avatar Sep 02 '20 02:09 offbynull

Here is the full example. offbynull-coroutines.zip

./classes are not in the classpath. They are only for demonstration. Clojure stores compiled sources in memory.

;; src/example/core.clj
(ns example.core
  (:import
   [com.offbynull.coroutines.user Continuation Coroutine CoroutineRunner]))

(set! *warn-on-reflection* true)

;; core$_main.class
(defn -main []
  (let [my-coroutine (reify Coroutine ;;core$_main$reify__<>.class
                       (run [_ c]
                         (.suspend c)))
        runner       (CoroutineRunner. my-coroutine)]
    (.execute runner)))

(comment
  (-main))
// decompiled by java decompiler
// the anonymous class was generated by the reify form 
package example;

import clojure.lang.IObj;
import clojure.lang.IPersistentMap;
import com.offbynull.coroutines.user.Continuation;
import com.offbynull.coroutines.user.Coroutine;

public final class core$_main$reify__12306 implements Coroutine, IObj {
  final IPersistentMap __meta;
  
  public core$_main$reify__12306(IPersistentMap paramIPersistentMap) {
    this.__meta = paramIPersistentMap;
  }
  
  public core$_main$reify__12306() {
    this(null);
  }
  
  public IPersistentMap meta() {
    return this.__meta;
  }
  
  public IObj withMeta(IPersistentMap paramIPersistentMap) {
    return new core$_main$reify__12306(paramIPersistentMap);
  }
  
  public void run(Continuation c) throws Exception {
    c = null;
    c
      
      .suspend();
    null;
  }
}
// decompiled main function

package example;

import clojure.lang.AFn;
import clojure.lang.AFunction;
import clojure.lang.IPersistentMap;
import clojure.lang.RT;
import com.offbynull.coroutines.user.Coroutine;
import com.offbynull.coroutines.user.CoroutineRunner;

public final class core$_main extends AFunction {
  public static final AFn const__4 = (AFn)RT.map(new Object[] { RT.keyword(null, "line"), Integer.valueOf(9), RT.keyword(null, "column"), Integer.valueOf(22) });
  
  public static Object invokeStatic() {
    Object my_coroutine = (new core$_main$reify__12306(null)).withMeta((IPersistentMap)const__4); // anonymous class instance 
    my_coroutine = null;
    Object runner = new CoroutineRunner((Coroutine)my_coroutine);    
    runner = null;
    return 
      
      ((CoroutineRunner)runner).execute() ? Boolean.TRUE : Boolean.FALSE;  // <= runner invocation
  }
  
  public Object invoke() {
    return invokeStatic();
  }
}

Execution error (NullPointerException) at example.core$_main$reify__148/run (core.clj:11).
null

Full report at:
/var/folders/j2/wjykw1xn5sz1c09j5zf_9ffc0000gn/T/clojure-17279943102503115240.edn
mikhail@iMac-Mihail offbynull-coroutines % open .
mikhail@iMac-Mihail offbynull-coroutines % cat /var/folders/j2/wjykw1xn5sz1c09j5zf_9ffc0000gn/T/clojure-3027632169497577500.edn
{:clojure.main/message
 "Execution error (NullPointerException) at example.core$_main$reify__148/run (core.clj:11).\nnull\n",
 :clojure.main/triage
 {:clojure.error/class java.lang.NullPointerException,
  :clojure.error/line 11,
  :clojure.error/symbol example.core$_main$reify__148/run,
  :clojure.error/source "core.clj",
  :clojure.error/phase :execution},
 :clojure.main/trace
 {:via
  [{:type com.offbynull.coroutines.user.CoroutineException,
    :message "Exception thrown during execution",
    :at
    [com.offbynull.coroutines.user.CoroutineRunner
     execute
     "CoroutineRunner.java"
     69]}
   {:type java.lang.NullPointerException,
    :at [example.core$_main$reify__148 run "core.clj" 11]}],
  :trace
  [[example.core$_main$reify__148 run "core.clj" 11]
   [com.offbynull.coroutines.user.CoroutineRunner
    execute
    "CoroutineRunner.java"
    65]
   [example.core$_main invokeStatic "core.clj" 13]
   [example.core$_main invoke "core.clj" 8]
   [clojure.lang.AFn applyToHelper "AFn.java" 152]
   [clojure.lang.AFn applyTo "AFn.java" 144]
   [clojure.lang.Var applyTo "Var.java" 705]
   [clojure.core$apply invokeStatic "core.clj" 665]
   [clojure.main$main_opt invokeStatic "main.clj" 514]
   [clojure.main$main_opt invoke "main.clj" 510]
   [clojure.main$main invokeStatic "main.clj" 664]
   [clojure.main$main doInvoke "main.clj" 616]
   [clojure.lang.RestFn applyTo "RestFn.java" 137]
   [clojure.lang.Var applyTo "Var.java" 705]
   [clojure.main main "main.java" 40]]}}

You can reproduce this

  1. download clojure CLI - https://clojure.org/guides/getting_started
  2. unzip archive
  3. cd offbynull-coroutines
  4. clojure -Adev -m example.core

-Adev setups the agent

;; deps.edn
{:deps    {}
 :aliases {:dev {:jvm-opts ["-javaagent:java-agent-1.5.3-shaded.jar"]}}}

darkleaf avatar Sep 02 '20 10:09 darkleaf