home..

Call Python function From Go!

In the last two posts we learnt how to call C code from Golang and how to call Python Code from C, respectively. This post will bring both of them together, here we are trying to call python bytecode from Golang. The following figure explains it well.

Calling Python ByteCode from Golang

The first part of the code is in golang as follows:

File: ~/go-py/main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package main

/*
#cgo pkg-config: python3
#cgo LDFLAGS: -lpython3.8

#include "glue.c"
*/
import "C"
import (
	"fmt"
	"log"
	"unsafe"
)

func Py_Initialize() {
	C.initialize()
}
func Py_Finalize() {
	C.finalize()
}
func pyError() error {
	pyerr := C.py_error()

	if pyerr == nil {
		return nil
	}

	err := C.GoString(pyerr)

	return fmt.Errorf("%s", err)
}

func close(fn *C.PyObject) {
	if fn == nil {
		return
	}
	C.py_decref(fn)
	fn = nil
}

func main() {
	Py_Initialize()
	defer Py_Finalize()
	fn, err := importPyFunc("Rectangle", "area")
	if err != nil {
		log.Fatal(err)
		return
	}
	x := 7.89
	y := 12.24
	area, err := callPyFunc(fn, x, y)
	if err != nil {
		log.Fatal(err)
		return
	}
	fmt.Printf("Area is %f\n", area)
}

func importPyFunc(moduleName, symbol string) (*C.PyObject, error) {
	cMod := C.CString(moduleName)
	cFunc := C.CString(symbol)

	defer func() {
		C.free(unsafe.Pointer(cMod))
		C.free(unsafe.Pointer(cFunc))
	}()

	fn := C.import_function(cMod, cFunc)
	if fn.err == 1 {
		return nil, pyError()
	}

	return fn.object, nil
}

func callPyFunc(fun *C.PyObject, x float64, y float64) (float64, error) {
	width := C.double(x)
	height := C.double(y)

	area := C.call_func(fun, width, height)

	close(fun)

	if area.err == 1 {
		return -1, pyError()
	}
	result := float64(area.val)
	return result, nil
}

The above code is go equivalent of the following python code:

import Rectangle
Rectangle.area(x,y)

This post has a detailed explaination on how to read/write cgo code.

All the C functions used in main.go are defined in the following glue.c file.

File: ~/go-py/glue.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#include<Python.h>

typedef struct{
    PyObject *object;
    double val;
    // TODO : Pass Custom error message from C to GO.
    int err;
} PyRes;

void initialize(){
    Py_Initialize();
}

void finalize(){
    Py_Finalize();
}

void py_decref(PyObject *obj){
    Py_DECREF(obj);
}

const char *py_error(){
    PyObject *err = PyErr_Occurred();
    if (err == NULL){
        return NULL;
    }

    PyObject *str = PyObject_Str(err);
    const char *utf8 = PyUnicode_AsUTF8(str);
    Py_DECREF(str);
    return utf8;
}

PyRes call_func(PyObject *func, double x, double y){
    PyObject *args;
    PyObject *kwargs;
    PyObject *result = 0;
    PyRes retval = {NULL,-1,0};

    if(!PyCallable_Check(func)){
        printf("call_func: func is not a callable PyObject \n");
        retval.err = 1;
        goto fail;
    }

    args = Py_BuildValue("(dd)",x,y);
    kwargs = NULL;

    result = PyObject_Call(func, args, kwargs);

    Py_DECREF(args);
    Py_XDECREF(kwargs);

    if(PyErr_Occurred()){
        retval.err = 1;
        goto fail;
    }

    if (!PyFloat_Check(result)){
        printf("call_func: callable didn't return a float\n");
        retval.err = 1;
        goto fail;
    }

    retval.val = PyFloat_AsDouble(result);
    Py_DECREF(result);

    if(PyErr_Occurred()){
        retval.err = 1;
        goto fail;
    }

    return retval;
    fail:
        Py_XDECREF(result);
        return retval;
}


PyRes import_function(const char *modname, const char *symbol){
    PyObject *u_name, *module;
    PyRes res = {NULL,-1,0};
    char buffer[1024];
    u_name = PyUnicode_FromString(modname);
    if(u_name == NULL){
        printf("import_function: Couldnot convert string \"%s\" to unicode ", modname);
        res.err = 1;
        return res;
    }

    module = PyImport_Import(u_name);
    Py_DECREF(u_name);

    if(module == NULL){
        
        printf("import_function: Couldnot import module %s ",modname);
        res.err = 1;
        return res;
    }
    
    res.object = PyObject_GetAttrString(module,symbol);
    if(res.object == NULL){
        printf("import_function: Couldnot Get the %s attribute from %s module",symbol,modname);
        res.err = 1;
        return res;
    }
    Py_DECREF(module);
    return res;
}

The glue.c file’s main purpose is to expose the CPYTHON API on Golang side. Some of the functions such as initialize, py_decref, finalize etc. are invoked from Golang side, but the main use of these functions is to make use of Cpython API.

The other functions such as import_function and call_func are used to import the python function Rectangle.area and call it respectively.

This post has a detailed explaination on how to read/write cpython code.

Finally we have our python function as follows:

File: ~/go-py/Rectangle.py
1
2
3
4
5
6
7
8
def area(x: float, y: float)->float:
    """
    @param x : width of rectangle
    @param y : height of rectangle
    @retval  : area of rectangle
    Calculates Area of a Triangle
    """
    return x * y

Here is a github repository for this project. It has a readme file which explains how to run this on your local system.

Fork Me On Github

References :

Using Python in memory

Embedding Python in Go

© 2022    •  Theme  Moonwalk