oracle / graalpython

A Python 3 implementation built on GraalVM
Other
1.2k stars 103 forks source link

Instantiating an imported Java class #344

Closed msalman-abid closed 1 year ago

msalman-abid commented 1 year ago

I've been facing difficulty with accessing methods of an exported Java class within the scope of the Python guest language.

For context, I have a class in Java named Point.java which has the following code:

Point.java

class Point {
    int x;
    int y;

    Point(int x, int y) {
        this(x, y, false);
    }

    static final double EPS = 0.0001;

    Point(double r, double a/* ngle */, boolean polar) {
        double x = polar ? r * Math.cos(a) : r;
        double y = polar ? r * Math.sin(a) : a;
        this.x = (int) x;
        this.y = (int) y;
        if (Math.abs(this.x - x) >= EPS || Math.abs(this.y - y) >= EPS) {
            throw new IllegalArgumentException("Likely not integers!");
        }
    }

    int getX() {
        return x;
    }

    int getY() {
        return y;
    }

    public String toString() {
        return "(" + x + ", " + y + ")";
    }
}

Now, I have another file named Segment.java (which works in conjunction with Segment.py) to create instances of the Point class within Python. Some code for both of these files is shared below:

Segment.java

class Segment {
    private static Value clz;

    private Value obj;

    static {
        try {
            File file = new File("subtask-1.5/src/main/python", "Segment.py");
            Source source = Source.newBuilder("python", file).build();
            assert source != null;

            // build context to get EPS
            Context context = Context.newBuilder("python")
                    .allowAllAccess(true)
                    .option("python.PythonPath", "subtask-1.5/src/main/python")
                    .build();

            // add Point class to context before evaluating Segment.py
            context.getPolyglotBindings().putMember("Point", Point.class);

            context.eval(source);

            clz = context.getBindings("python").getMember("Segment");

        } catch (Exception e) {
            System.out.println("[-] " + e);
        }
    }

    Segment(Point p1, Point p2) {
        try {
            obj = clz.execute(p1, p2);
        } catch (PolyglotException e) {
            System.out.println("[-] " + e);
            throw new IllegalArgumentException("Points must differ!");
        }
    }
}

Segment.py

(This also has other code, but it is not relevant to the scope of this issue)

x = polyglot.import_value("Point")

x(1,2)
# Result: invalid instantiation of foreign object

I tried to replicate examples from the documentation on this python API found here, but I'm not certain how to access the constructor or other methods of this imported class.

The output from the dir() function in Python lists all available methods on the object, but I cannot figure out if I can call a constructor using any of these. Can you please tell me what I'm missing?

msimacek commented 1 year ago

Hi @msalman-abid, your Point class and its constructor should be declared public otherwise python can't access them.

Btw, we have the java module (documented a bit later in the same interop doc) which allows you to load Java classes from the classpath, so you can do java.type("Point") without creating any binding on the Java side.

msalman-abid commented 1 year ago

I'm grateful for your help @msimacek.

Adding the public access modifier to the class solved the issue, and I'm able to now pass Point objects to Python directly (with support for their methods).

These are now working:

class Segment:
    def __init__(self, p1, p2) -> None:
        self.p1 = p1
        self.p2 = p2        

    def getP1(self):
        return self.p1

    def getP2(self):
        return self.p2

    def length(self):
        a = math.pow(self.p1.getX() - self.p2.getX(), 2)
        b = math.pow(self.p1.getY() - self.p2.getY(), 2)
        return math.sqrt(a + b)