Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

x/mobile/bind/seq: Seq can throw "java.lang.RuntimeException: unknown java Ref" when a legitimate Java object is passed to Go #10933

Closed
rod-hynes opened this issue May 22, 2015 · 2 comments

Comments

@rod-hynes
Copy link

Summary

When a Java stub object instance is passed into Go, and then Go's garbage collection runs, and then the same Java object is passed into Go again, an exception is thrown in go.Seq.java and the Android app crashes.

Test setup

  • Go tip: 91191e7
  • golang.org/x/mobile: 73e04c3da0a52c9bc644e1bf696712facae30488
  • Android 5.1 on a Nexus 5
  • Build using gomobile bind and import resulting AAR into an Android Studio project
  • Sample code follows; based on golang.org/x/mobile/example/libhello/hi. This is a contrived example with an explicit GC() call, but I've also hit this exception -- eventually -- in real code.

As best as I can tell, the problem may be a lifetime mismatch in the go.Seq.Ref reference counting. The constructor of the Stub creates a reference. When the Java object reference is passed to Go, the reference is scheduled for deletion using runtime.SetFinalizer here. The SetFinalizer handler signals the Java reference manager to delete the reference. But since the reference is only added in the Java Stub object constructor, there's no valid reference when the Java object is used again after the SetFinalizer signal and the exception will be thrown.

The docs on passing foreign language objects to Go don't indicate that this reuse of a Java Stub object is not supported.

One workaround is to create a new, proxy Java object each time you wish to pass the same long-lived Java object into Go.

Exception

05-21 15:52:16.957    2280-2313/ca.psiphon.androidgolanghello E/RECEIVER-HELLO﹕ Hello, a!
05-21 15:52:16.958    2280-2313/ca.psiphon.androidgolanghello E/RECEIVER-HELLO﹕ Hello, b!
05-21 15:52:16.961    2280-2313/ca.psiphon.androidgolanghello E/AndroidRuntime﹕ FATAL EXCEPTION: pool-1-thread-1
    Process: ca.psiphon.androidgolanghello, PID: 2280
    java.lang.RuntimeException: unknown java Ref: 42
            at go.Seq$RefTracker.get(Seq.java:252)
            at go.Seq$1.run(Seq.java:126)
            at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
            at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
            at java.lang.Thread.run(Thread.java:818)

Sample Go package

package hirecv

import (
    "fmt"
    "runtime"
)

type Receiver interface {
    Hello(message string)
}

func Hello(r Receiver, name string) {
    r.Hello(fmt.Sprintf("Hello, %s!\n", name))
}

func GarbageCollect() {
    runtime.GC()
}

Sample Android app using the Go package

public class MainActivity extends ActionBarActivity {

    static class Receiver extends Hirecv.Receiver.Stub {
        @Override
        public void Hello(String s) {
            android.util.Log.e("RECEIVER-HELLO", s);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Go.init(this);

        Receiver receiver = new Receiver();
        Hirecv.Hello(receiver, "a");
        Hirecv.Hello(receiver, "b");
        Hirecv.GarbageCollect();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
        }
        Hirecv.Hello(receiver, "c");
    }
...
@ianlancetaylor
Copy link
Contributor

CC @crawshaw @hyangah

@rakyll rakyll changed the title mobile/bind/seq: Seq can throw "java.lang.RuntimeException: unknown java Ref" when a legitimate Java object is passed to Go x/mobile/bind/seq: Seq can throw "java.lang.RuntimeException: unknown java Ref" when a legitimate Java object is passed to Go May 24, 2015
@ianlancetaylor ianlancetaylor added this to the Unreleased milestone Jun 3, 2015
@gopherbot
Copy link

CL https://golang.org/cl/10638 mentions this issue.

@golang golang locked and limited conversation to collaborators Jun 25, 2016
imWildCat pushed a commit to imWildCat/go-mobile that referenced this issue Apr 10, 2021
The gobind framework is supposed to use reference counting to
keep track of objects (e.g. pointer to a Go struct, interface
values) crossing the language boundary. This change fixes two bugs:

1) no reference counting on Java object: Previously, the lifetime
of a Java object was manages in the following way.

 a. The Java object is pinned in an internal map (javaObjs) when it's
constructed.
 b. When Go receives the reference to the Java object, it creates a
proxy object and sets a finalizer on it. The finalizer signals Java
to unpin the Java object (remove from the javaObjs map).
 c. The javaObjs map is also used to identify the Java object when
Go asks to invoke a method on it later.

When the same Java object is sent to Java more than once, and the
finalizer (b) runs after the first use, the second use of the Java
object can cause the crash described in golang/go#10933.

This change fixes the bug by reference counting the Java object.
Java side pins the Java object and increments the refcount whenever it
sees the object sent to Go (in Seq.writeRef). When the Go proxy
object's finalizer runs, the refcount is decremented. When the refcount
becomes 0, the object gets unpined.

2) race in Go object lifetime management: Pinning on a Go object
has been done when the Go object is sent to Java but the Go object
is not in the pinned object map yet. (bind/seq.WriteGoRef).
Unpinning the object occurs when Java finds there are no proxy objects
on its side. For this, Java maintains a reference count map (goObjs).
When the refcount becomes zero, Java notifies Go so the object is
unpinned. Here is a race case:

 a. Java has a proxy object for a Go object.
 b. Go is preparing for sending the same Go object. seq.WriteGoRef
notices the corresponding entry in the pinned object map already,
and returns. The remaining work for sending the object continues.
 c. The proxy object in Java finalizes and triggers deletion of the
object from the pinned object map.
 d. The remaining work for (b) completes and Java creates a new proxy
object. When a method is called for the Go object, the Go object is
already removed from the object map on Go side and maybe already GC'd.

This change fixes it by converting the pinned object map to reference
counter map maintained in Go. The counter increments for each
seq.WriteGoRef call. The finalizer of the proxy object in Java causes
a decrement of the counter.

Fixes golang/go#10933.

Renables the skipped testJavaRefGC.

Change-Id: I0992e002b1050b6183689e5ab821e058adbb420f
Reviewed-on: https://go-review.googlesource.com/10638
Reviewed-by: David Crawshaw <crawshaw@golang.org>
imWildCat pushed a commit to imWildCat/go-mobile that referenced this issue Apr 11, 2021
The gobind framework is supposed to use reference counting to
keep track of objects (e.g. pointer to a Go struct, interface
values) crossing the language boundary. This change fixes two bugs:

1) no reference counting on Java object: Previously, the lifetime
of a Java object was manages in the following way.

 a. The Java object is pinned in an internal map (javaObjs) when it's
constructed.
 b. When Go receives the reference to the Java object, it creates a
proxy object and sets a finalizer on it. The finalizer signals Java
to unpin the Java object (remove from the javaObjs map).
 c. The javaObjs map is also used to identify the Java object when
Go asks to invoke a method on it later.

When the same Java object is sent to Java more than once, and the
finalizer (b) runs after the first use, the second use of the Java
object can cause the crash described in golang/go#10933.

This change fixes the bug by reference counting the Java object.
Java side pins the Java object and increments the refcount whenever it
sees the object sent to Go (in Seq.writeRef). When the Go proxy
object's finalizer runs, the refcount is decremented. When the refcount
becomes 0, the object gets unpined.

2) race in Go object lifetime management: Pinning on a Go object
has been done when the Go object is sent to Java but the Go object
is not in the pinned object map yet. (bind/seq.WriteGoRef).
Unpinning the object occurs when Java finds there are no proxy objects
on its side. For this, Java maintains a reference count map (goObjs).
When the refcount becomes zero, Java notifies Go so the object is
unpinned. Here is a race case:

 a. Java has a proxy object for a Go object.
 b. Go is preparing for sending the same Go object. seq.WriteGoRef
notices the corresponding entry in the pinned object map already,
and returns. The remaining work for sending the object continues.
 c. The proxy object in Java finalizes and triggers deletion of the
object from the pinned object map.
 d. The remaining work for (b) completes and Java creates a new proxy
object. When a method is called for the Go object, the Go object is
already removed from the object map on Go side and maybe already GC'd.

This change fixes it by converting the pinned object map to reference
counter map maintained in Go. The counter increments for each
seq.WriteGoRef call. The finalizer of the proxy object in Java causes
a decrement of the counter.

Fixes golang/go#10933.

Renables the skipped testJavaRefGC.

Change-Id: I0992e002b1050b6183689e5ab821e058adbb420f
Reviewed-on: https://go-review.googlesource.com/10638
Reviewed-by: David Crawshaw <crawshaw@golang.org>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

4 participants