From 0f7f2de4e6c2f3b4247f498f525fcadf17bc17a4 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 30 Oct 2024 09:28:36 +0800 Subject: [PATCH] initial --- .gitignore | 4 + README.md | 88 ++++++++++ _demo/gradio/gradio.go | 84 ++++++++++ _demo/plot/plot.go | 39 +++++ adap_go.go | 33 ++++ bool.go | 43 +++++ builder.go | 123 ++++++++++++++ bytes.go | 37 +++++ complex.go | 33 ++++ dict.go | 70 ++++++++ float.go | 28 ++++ function.go | 175 ++++++++++++++++++++ go.mod | 3 + list.go | 42 +++++ long.go | 56 +++++++ math/math.go | 18 +++ module.go | 43 +++++ object.go | 359 +++++++++++++++++++++++++++++++++++++++++ python.go | 96 +++++++++++ tuple.go | 114 +++++++++++++ unicode.go | 42 +++++ 21 files changed, 1530 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 _demo/gradio/gradio.go create mode 100644 _demo/plot/plot.go create mode 100644 adap_go.go create mode 100644 bool.go create mode 100644 builder.go create mode 100644 bytes.go create mode 100644 complex.go create mode 100644 dict.go create mode 100644 float.go create mode 100644 function.go create mode 100644 go.mod create mode 100644 list.go create mode 100644 long.go create mode 100644 math/math.go create mode 100644 module.go create mode 100644 object.go create mode 100644 python.go create mode 100644 tuple.go create mode 100644 unicode.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb18a1e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.tool-versions +*.out +*.pyc +__pycache__ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3417de8 --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +## Goal + +- Provide automatically DecRef for Python objects. +- Wrap generic PyObject(s) to typed Python objects. +- Provide a way to define Python objects in LLGo. + +## Python types wrapper design + +To automatically DecRef Python objects, we need to wrap them in a Go struct that will call DecRef when it is garbage collected. This is done by embedding a PyObject in a Go struct and registering a finalizer on the Go struct. Below is an example of how this is done: + +```go +type pyObject struct { + obj *C.PyObject +} + +func newObject(obj *C.PyObject) *pyObject { + o := &pyObject{obj} + runtime.SetFinalizer(o, func(o *pyObject) { + o.obj.DecRef() + }) + return o +} +``` + +To wrap generic PyObject(s) to typed Python objects, the best way is using alias types. Below is an example of how this is done: + +```go +type Object *pyObject + +func (o Object) GetAttrString(name string) Object { + return newObject(o.obj.GetAttrString(name)) +} + +type Dict Object + +func (d Dict) SetItemString(name string, value Object) { + d.obj.SetItemString(name, value.obj) +} +``` + +Unfortunately, Go does not allow defining methods on alias types like the above. + +```shell +invalid receiver type PyObject (pointer or interface type) +invalid receiver type PyDict (pointer or interface type) +``` + +We can define a new type that embeds the alias type and define methods on the new type. Below is an example of how this is done: + +```go +type Object struct { + *pyObject +} + +func (o *Object) GetAttrString(name string) *Object { + return &Object{newObject(o.obj.GetAttrString(name))} +} + +type Dict struct { + *Object +} + +func (d *Dict) SetItemString(name string, value *Object) { + d.obj.SetItemString(name, value.obj) +} +``` + +But allocating a `PyDict` object will allocate a `PyObject` object and a `pyObject` object. This is not efficient. + +We can use a `struct` instead of a `pointer` to avoid this. Below is an example of how this is done: + +```go +type Object struct { + *pyObject +} + +func (o Object) GetAttrString(name string) Object { + return Object{newObject(o.obj.GetAttrString(name))} +} + +type Dict struct { + Object +} + +func (d Dict) SetItemString(name string, value Object) { + d.obj.SetItemString(name, value.obj) +} +``` diff --git a/_demo/gradio/gradio.go b/_demo/gradio/gradio.go new file mode 100644 index 0000000..eac5b5f --- /dev/null +++ b/_demo/gradio/gradio.go @@ -0,0 +1,84 @@ +package main + +/* +#cgo pkg-config: python-3.12-embed +#include + +extern PyObject* UpdateExamples2(PyObject* self, PyObject* args); +*/ +import "C" + +import ( + "os" + "unsafe" + + "github.com/cpunion/go-python" +) + +/* +import gradio as gr + +def update_examples(country): + if country == "USA": + return gr.Dataset(samples=[["Chicago"], ["Little Rock"], ["San Francisco"]]) + else: + return gr.Dataset(samples=[["Islamabad"], ["Karachi"], ["Lahore"]]) + +with gr.Blocks() as demo: + dropdown = gr.Dropdown(label="Country", choices=["USA", "Pakistan"], value="USA") + textbox = gr.Textbox() + examples = gr.Examples([["Chicago"], ["Little Rock"], ["San Francisco"]], textbox) + dropdown.change(update_examples, dropdown, examples.dataset) + +demo.launch() +*/ + +var gr python.Module + +func UpdateExamples(country string) python.Object { + println("country:", country) + if country == "USA" { + return gr.CallKeywords("Dataset")(python.MakeDict(map[any]any{ + "samples": [][]string{{"Chicago"}, {"Little Rock"}, {"San Francisco"}}, + })) + } else { + return gr.CallKeywords("Dataset")(python.MakeDict(map[any]any{ + "samples": [][]string{{"Islamabad"}, {"Karachi"}, {"Lahore"}}, + })) + } +} + +//export UpdateExamples2 +func UpdateExamples2(self, args *C.PyObject) *C.PyObject { + argsTuple := python.FromPy((*python.PyObject)(unsafe.Pointer(args))).AsTuple() + country := argsTuple.Get(0).String() + return (*C.PyObject)(unsafe.Pointer(UpdateExamples(country).Obj())) +} + +func main() { + if len(os.Args) > 2 { + // avoid gradio start subprocesses + return + } + + python.Initialize() + gr = python.ImportModule("gradio") + fn := python.FuncOf("update_examples", UpdateExamples, + "update_examples(country, /)\n--\n\nUpdate examples based on country") + // fn := python.FuncOf1("update_examples", unsafe.Pointer(C.UpdateExamples2), + // "update_examples(country, /)\n--\n\nUpdate examples based on country") + // fn := python.FuncOf(UpdateExamples) + blocks := gr.Call("Blocks") + demo := python.With(blocks, func(v python.Object) { + dropdown := gr.CallKeywords("Dropdown")(python.MakeDict(map[any]any{ + "label": "Country", + "choices": []string{"USA", "Pakistan"}, + "value": "USA", + })) + textbox := gr.Call("Textbox") + examples := gr.Call("Examples", [][]string{{"Chicago"}, {"Little Rock"}, {"San Francisco"}}, textbox) + dataset := examples.GetAttr("dataset") + dropdown.CallMethod("change", fn, dropdown, dataset) + }) + demo.CallMethod("launch") +} diff --git a/_demo/plot/plot.go b/_demo/plot/plot.go new file mode 100644 index 0000000..258327f --- /dev/null +++ b/_demo/plot/plot.go @@ -0,0 +1,39 @@ +package main + +import ( + "github.com/cpunion/go-python" +) + +type plt struct { + python.Module +} + +func Plt() plt { + return plt{python.ImportModule("matplotlib.pyplot")} +} + +func (m plt) Plot(args ...any) func(kw any) python.Object { + return m.CallKeywords("plot", args...) +} + +func (m plt) Show() { + m.Call("show") +} + +func plot1() { + plt := python.ImportModule("matplotlib.pyplot") + plt.CallKeywords("plot", python.MakeTuple(5, 10), python.MakeTuple(10, 15))(python.DictFromPairs("color", "red")) + plt.Call("show") +} + +func plot2() { + plt := Plt() + plt.Plot(python.MakeTuple(5, 10), python.MakeTuple(10, 15))(python.DictFromPairs("color", "red")) + plt.Show() +} + +func main() { + python.Initialize() + plot1() + plot2() +} diff --git a/adap_go.go b/adap_go.go new file mode 100644 index 0000000..6845d48 --- /dev/null +++ b/adap_go.go @@ -0,0 +1,33 @@ +package python + +/* +#cgo pkg-config: python-3.12-embed +#include +*/ +import "C" +import ( + "unsafe" +) + +//go:inline +func AllocCStr(s string) *C.char { + return C.CString(s) +} + +func AllocWCStr(s string) *C.wchar_t { + runes := []rune(s) + wchars := make([]uint16, len(runes)+1) + for i, r := range runes { + wchars[i] = uint16(r) + } + wchars[len(runes)] = 0 + return (*C.wchar_t)(unsafe.Pointer(&wchars[0])) +} + +func GoString(s *C.char) string { + return C.GoString((*C.char)(s)) +} + +func GoStringN(s *C.char, n int) string { + return C.GoStringN((*C.char)(s), C.int(n)) +} diff --git a/bool.go b/bool.go new file mode 100644 index 0000000..2a4ff03 --- /dev/null +++ b/bool.go @@ -0,0 +1,43 @@ +package python + +/* +#cgo pkg-config: python-3.12-embed +#include +*/ +import "C" + +type Bool struct { + Object +} + +func newBool(obj *C.PyObject) Bool { + return Bool{newObject(obj)} +} + +func MakeBool(b bool) Bool { + if b { + return True() + } + return False() +} + +var trueObj Object +var falseObj Object + +func True() Bool { + if trueObj.Nil() { + trueObj = MainModule().Dict().GetString("True") + } + return trueObj.AsBool() +} + +func False() Bool { + if falseObj.Nil() { + falseObj = MainModule().Dict().GetString("False") + } + return falseObj.AsBool() +} + +func (b Bool) Bool() bool { + return C.PyObject_IsTrue(b.obj) != 0 +} diff --git a/builder.go b/builder.go new file mode 100644 index 0000000..23950e2 --- /dev/null +++ b/builder.go @@ -0,0 +1,123 @@ +package python + +/* +#cgo pkg-config: python-3.12-embed +#include +#include + +static PyModuleDef_Base moduleHeadInit() { + PyModuleDef_Base base = PyModuleDef_HEAD_INIT; + return base; +} + +*/ +import "C" +import ( + "fmt" +) + +// ModuleBuilder helps to build Python modules +type ModuleBuilder struct { + name string + doc string + methods []C.PyMethodDef +} + +// NewModuleBuilder creates a new ModuleBuilder +func NewModuleBuilder(name, doc string) *ModuleBuilder { + return &ModuleBuilder{ + name: name, + doc: doc, + } +} + +// /* Flag passed to newmethodobject */ +// /* #define METH_OLDARGS 0x0000 -- unsupported now */ +// #define METH_VARARGS 0x0001 +// #define METH_KEYWORDS 0x0002 +// /* METH_NOARGS and METH_O must not be combined with the flags above. */ +// #define METH_NOARGS 0x0004 +// #define METH_O 0x0008 + +// /* METH_CLASS and METH_STATIC are a little different; these control +// the construction of methods for a class. These cannot be used for +// functions in modules. */ +// #define METH_CLASS 0x0010 +// #define METH_STATIC 0x0020 + +// /* METH_COEXIST allows a method to be entered even though a slot has +// already filled the entry. When defined, the flag allows a separate +// method, "__contains__" for example, to coexist with a defined +// slot like sq_contains. */ + +// #define METH_COEXIST 0x0040 + +// #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030a0000 +// # define METH_FASTCALL 0x0080 +// #endif + +// /* This bit is preserved for Stackless Python */ +// #ifdef STACKLESS +// # define METH_STACKLESS 0x0100 +// #else +// # define METH_STACKLESS 0x0000 +// #endif + +// /* METH_METHOD means the function stores an +// * additional reference to the class that defines it; +// * both self and class are passed to it. +// * It uses PyCMethodObject instead of PyCFunctionObject. +// * May not be combined with METH_NOARGS, METH_O, METH_CLASS or METH_STATIC. +// */ + +// #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x03090000 +// #define METH_METHOD 0x0200 +// #endif + +const ( + METH_VARARGS = 0x0001 + METH_KEYWORDS = 0x0002 + METH_NOARGS = 0x0004 + METH_O = 0x0008 + METH_CLASS = 0x0010 + METH_STATIC = 0x0020 + METH_COEXIST = 0x0040 + METH_FASTCALL = 0x0080 + METH_METHOD = 0x0200 +) + +// AddMethod adds a method to the module +func (mb *ModuleBuilder) AddMethod(name string, fn C.PyCFunction, doc string) *ModuleBuilder { + mb.methods = append(mb.methods, C.PyMethodDef{ + ml_name: AllocCStr(name), + ml_meth: fn, + ml_flags: METH_VARARGS, + ml_doc: AllocCStr(doc), + }) + return mb +} + +// Build creates and returns a new Python module +func (mb *ModuleBuilder) Build() Module { + // Add a null terminator to the methods slice + mb.methods = append(mb.methods, C.PyMethodDef{}) + def := &C.PyModuleDef{ + m_base: C.moduleHeadInit(), + m_name: AllocCStr(mb.name), + m_doc: AllocCStr(mb.doc), + m_size: -1, + m_methods: &mb.methods[0], + } + fmt.Printf("name: %s, doc: %s, size: %d\n", GoString(def.m_name), GoString(def.m_doc), def.m_size) + for _, m := range mb.methods { + fmt.Printf("method: %s, doc: %s\n", GoString(m.ml_name), GoString(m.ml_doc)) + } + + m := C.PyModule_Create2(def, 1013) + + if m == nil { + panic("failed to create module") + } + + return newModule(m) +} diff --git a/bytes.go b/bytes.go new file mode 100644 index 0000000..2dae9d9 --- /dev/null +++ b/bytes.go @@ -0,0 +1,37 @@ +package python + +/* +#cgo pkg-config: python-3.12-embed +#include +*/ +import "C" +import ( + "unsafe" +) + +type Bytes struct { + Object +} + +func newBytes(obj *C.PyObject) Bytes { + return Bytes{newObject(obj)} +} + +func BytesFromStr(s string) Bytes { + return MakeBytes([]byte(s)) +} + +func MakeBytes(bytes []byte) Bytes { + ptr := C.CBytes(bytes) + return newBytes(C.PyBytes_FromStringAndSize((*C.char)(ptr), C.Py_ssize_t(len(bytes)))) +} + +func (b Bytes) Bytes() []byte { + var p *byte + var l int + return C.GoBytes(unsafe.Pointer(p), C.int(l)) +} + +func (b Bytes) Decode(encoding string) Str { + return Cast[Str](b.CallMethod("decode", MakeStr(encoding))) +} diff --git a/complex.go b/complex.go new file mode 100644 index 0000000..abcf98a --- /dev/null +++ b/complex.go @@ -0,0 +1,33 @@ +package python + +/* +#cgo pkg-config: python-3.12-embed +#include +*/ +import "C" + +type Complex struct { + Object +} + +func newComplex(obj *C.PyObject) Complex { + return Complex{newObject(obj)} +} + +func MakeComplex(f complex128) Complex { + return newComplex(C.PyComplex_FromDoubles(C.double(real(f)), C.double(imag(f)))) +} + +func (c Complex) Complex128() complex128 { + real := C.PyComplex_RealAsDouble(c.obj) + imag := C.PyComplex_ImagAsDouble(c.obj) + return complex(real, imag) +} + +func (c Complex) Real() float64 { + return float64(C.PyComplex_RealAsDouble(c.obj)) +} + +func (c Complex) Imag() float64 { + return float64(C.PyComplex_ImagAsDouble(c.obj)) +} diff --git a/dict.go b/dict.go new file mode 100644 index 0000000..e981964 --- /dev/null +++ b/dict.go @@ -0,0 +1,70 @@ +package python + +/* +#cgo pkg-config: python-3.12-embed +#include +*/ +import "C" + +type Dict struct { + Object +} + +func newDict(obj *C.PyObject) Dict { + return Dict{newObject(obj)} +} + +func NewDict(obj *C.PyObject) Dict { + return newDict(obj) +} + +func DictFromPairs(pairs ...any) Dict { + if len(pairs)%2 != 0 { + panic("DictFromPairs requires an even number of arguments") + } + dict := newDict(C.PyDict_New()) + for i := 0; i < len(pairs); i += 2 { + key := From(pairs[i]) + value := From(pairs[i+1]) + dict.Set(key, value) + } + return dict +} + +func MakeDict(m map[any]any) Dict { + dict := newDict(C.PyDict_New()) + for key, value := range m { + keyObj := From(key) + valueObj := From(value) + dict.Set(keyObj, valueObj) + } + return dict +} + +func (d Dict) Get(key Objecter) Object { + v := C.PyDict_GetItem(d.obj, key.Obj()) + C.Py_IncRef(v) + return newObject(v) +} + +func (d Dict) Set(key, value Object) { + C.Py_IncRef(key.obj) + C.Py_IncRef(value.obj) + C.PyDict_SetItem(d.obj, key.obj, value.obj) +} + +func (d Dict) SetString(key string, value Object) { + C.Py_IncRef(value.obj) + C.PyDict_SetItemString(d.obj, AllocCStr(key), value.obj) +} + +func (d Dict) GetString(key string) Object { + v := C.PyDict_GetItemString(d.obj, AllocCStr(key)) + C.Py_IncRef(v) + return newObject(v) +} + +func (d Dict) Del(key Object) { + C.PyDict_DelItem(d.obj, key.obj) + C.Py_DecRef(key.obj) +} diff --git a/float.go b/float.go new file mode 100644 index 0000000..a1946ff --- /dev/null +++ b/float.go @@ -0,0 +1,28 @@ +package python + +/* +#cgo pkg-config: python-3.12-embed +#include +*/ +import "C" + +type Float struct { + Object +} + +func newFloat(obj *C.PyObject) Float { + return Float{newObject(obj)} +} + +func MakeFloat(f float64) Float { + return newFloat(C.PyFloat_FromDouble(C.double(f))) +} + +func (f Float) Float64() float64 { + return float64(C.PyFloat_AsDouble(f.obj)) +} + +func (f Float) IsInteger() Bool { + fn := Cast[Func](f.GetAttr("is_integer")) + return Cast[Bool](fn.callNoArgs()) +} diff --git a/function.go b/function.go new file mode 100644 index 0000000..68fb9ac --- /dev/null +++ b/function.go @@ -0,0 +1,175 @@ +package python + +/* +#cgo pkg-config: python-3.12-embed +#include + +extern PyObject* wrapperFunc(PyObject* self, PyObject* args); +*/ +import "C" + +import ( + "fmt" + "reflect" + "unsafe" +) + +type Objecter interface { + Obj() *C.PyObject + object() Object + Ensure() +} + +type Func struct { + Object +} + +func newFunc(obj *C.PyObject) Func { + return Func{newObject(obj)} +} + +func (f Func) Ensure() { + f.pyObject.Ensure() +} + +func (f Func) call(args Tuple, kwargs Dict) Object { + return newObject(C.PyObject_Call(f.obj, args.obj, kwargs.obj)) +} + +func (f Func) callNoArgs() Object { + return newObject(C.PyObject_CallNoArgs(f.obj)) +} + +func (f Func) callOneArg(arg Objecter) Object { + return newObject(C.PyObject_CallOneArg(f.obj, arg.Obj())) +} + +func (f Func) CallObject(args Tuple) Object { + return newObject(C.PyObject_CallObject(f.obj, args.obj)) +} + +func (f Func) Call(args ...any) Object { + switch len(args) { + case 0: + return f.callNoArgs() + case 1: + return f.callOneArg(From(args[0])) + default: + argsTuple := C.PyTuple_New(C.Py_ssize_t(len(args))) + for i, arg := range args { + obj := From(arg).Obj() + C.Py_IncRef(obj) + C.PyTuple_SetItem(argsTuple, C.Py_ssize_t(i), obj) + } + return newObject(C.PyObject_CallObject(f.obj, argsTuple)) + } +} + +// ---------------------------------------------------------------------------- + +type wrapperContext struct { + v any +} + +//export wrapperFunc +func wrapperFunc(self, args *C.PyObject) *C.PyObject { + wCtx := (*wrapperContext)(C.PyCapsule_GetPointer(self, AllocCStr("wrapperContext"))) + fmt.Printf("wrapperContext: %p\n", wCtx) + // 恢复上下文 + v := reflect.ValueOf(wCtx.v) + t := v.Type() + fmt.Printf("wrapperFunc type: %v\n", t) + // 构建参数 + goArgs := make([]reflect.Value, t.NumIn()) + argsTuple := FromPy(args).AsTuple() + fmt.Printf("args: %v\n", argsTuple) + for i := range goArgs { + goArgs[i] = reflect.New(t.In(i)).Elem() + ToValue(FromPy(C.PyTuple_GetItem(args, C.Py_ssize_t(i))), goArgs[i]) + fmt.Printf("goArgs[%d]: %T\n", i, goArgs[i].Interface()) + } + + // 调用原始函数 + results := v.Call(goArgs) + + // 处理返回值 + if len(results) == 0 { + return None().Obj() + } + if len(results) == 1 { + return From(results[0].Interface()).Obj() + } + tuple := MakeTupleWithLen(len(results)) + for i := range results { + tuple.Set(i, From(results[i].Interface())) + } + return tuple.Obj() +} + +func FuncOf1(name string, fn unsafe.Pointer, doc string) Func { + def := &C.PyMethodDef{ + ml_name: AllocCStr(name), + ml_meth: C.PyCFunction(fn), + ml_flags: C.METH_VARARGS, + ml_doc: AllocCStr(doc), + } + pyFn := C.PyCMethod_New(def, nil, nil, nil) + return newFunc(pyFn) +} + +var ctxs = make(map[unsafe.Pointer]*wrapperContext) + +func FuncOf(name string, fn any, doc string) Func { + m := MainModule() + v := reflect.ValueOf(fn) + t := v.Type() + if t.Kind() != reflect.Func { + fmt.Printf("type: %T, kind: %d\n", fn, t.Kind()) + panic("AddFunction: fn must be a function") + } + println("FuncOf name:", name) + fmt.Printf("FuncOf type: %v\n", t) + ctx := new(wrapperContext) + ctx.v = fn + obj := C.PyCapsule_New(unsafe.Pointer(ctx), AllocCStr("wrapperContext"), nil) + fmt.Printf("FuncOf ctx: %p\n", ctx) + ctxs[unsafe.Pointer(ctx)] = ctx + def := &C.PyMethodDef{ + ml_name: AllocCStr(name), + ml_meth: C.PyCFunction(C.wrapperFunc), + ml_flags: C.METH_VARARGS, + ml_doc: AllocCStr(doc), + } + pyFn := C.PyCMethod_New(def, obj, m.obj, nil) + if pyFn == nil { + panic(fmt.Sprintf("Failed to add function %s to module", name)) + } + return newFunc(pyFn) +} + +func buildFormatString(t reflect.Type) *C.char { + format := "" + for i := 0; i < t.NumIn(); i++ { + switch t.In(i).Kind() { + case reflect.Int, reflect.Int64: + format += "i" + case reflect.Float64: + format += "d" + case reflect.String: + format += "s" + // Add more types as needed + default: + panic(fmt.Sprintf("Unsupported argument type: %v", t.In(i))) + } + } + return AllocCStr(format) +} + +func buildArgPointers(args []reflect.Value) []interface{} { + pointers := make([]interface{}, len(args)) + for i := range args { + args[i] = reflect.New(args[i].Type()).Elem() + pointers[i] = args[i].Addr().Interface() + } + return pointers +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3873b45 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/cpunion/go-python + +go 1.20 diff --git a/list.go b/list.go new file mode 100644 index 0000000..e61815a --- /dev/null +++ b/list.go @@ -0,0 +1,42 @@ +package python + +/* +#cgo pkg-config: python-3.12-embed +#include +*/ +import "C" + +type List struct { + Object +} + +func newList(obj *C.PyObject) List { + return List{newObject(obj)} +} + +func MakeList(args ...any) List { + list := newList(C.PyList_New(C.Py_ssize_t(len(args)))) + for i, arg := range args { + obj := From(arg) + list.SetItem(i, obj) + } + return list +} + +func (l List) GetItem(index int) Object { + v := C.PyList_GetItem(l.obj, C.Py_ssize_t(index)) + C.Py_IncRef(v) + return newObject(v) +} + +func (l List) SetItem(index int, item Object) { + C.PyList_SetItem(l.obj, C.Py_ssize_t(index), item.obj) +} + +func (l List) Len() int { + return int(C.PyList_Size(l.obj)) +} + +func (l List) Append(obj Object) { + C.PyList_Append(l.obj, obj.obj) +} diff --git a/long.go b/long.go new file mode 100644 index 0000000..0b2f805 --- /dev/null +++ b/long.go @@ -0,0 +1,56 @@ +package python + +/* +#cgo pkg-config: python-3.12-embed +#include +*/ +import "C" + +type Long struct { + Object +} + +func newLong(obj *C.PyObject) Long { + return Long{newObject(obj)} +} + +func MakeLong(i int64) Long { + return newLong(C.PyLong_FromLongLong(C.longlong(i))) +} + +func (l Long) Int64() int64 { + return int64(C.PyLong_AsLongLong(l.obj)) +} + +func (l Long) Uint64() uint64 { + return uint64(C.PyLong_AsUnsignedLongLong(l.obj)) +} + +func (l Long) AsFloat64() float64 { + return float64(C.PyLong_AsDouble(l.obj)) +} + +func LongFromFloat64(v float64) Long { + return newLong(C.PyLong_FromDouble(C.double(v))) +} + +func LongFromString(s string, base int) Long { + cstr := AllocCStr(s) + return newLong(C.PyLong_FromString(cstr, nil, C.int(base))) +} + +func LongFromUnicode(u Object, base int) Long { + return newLong(C.PyLong_FromUnicodeObject(u.Obj(), C.int(base))) +} + +func (l Long) AsUint64() uint64 { + return uint64(C.PyLong_AsUnsignedLongLong(l.obj)) +} + +func (l Long) AsUintptr() uintptr { + return uintptr(C.PyLong_AsLong(l.obj)) +} + +func LongFromUintptr(v uintptr) Long { + return newLong(C.PyLong_FromLong(C.long(v))) +} diff --git a/math/math.go b/math/math.go new file mode 100644 index 0000000..b5504b0 --- /dev/null +++ b/math/math.go @@ -0,0 +1,18 @@ +package math + +import ( + "github.com/cpunion/go-python" +) + +var math_ python.Module + +func math() python.Module { + if math_.Nil() { + math_ = python.ImportModule("math") + } + return math_ +} + +func Sqrt(x python.Float) python.Float { + return math().CallMethod("sqrt", x.Obj()).AsFloat() +} diff --git a/module.go b/module.go new file mode 100644 index 0000000..01af318 --- /dev/null +++ b/module.go @@ -0,0 +1,43 @@ +package python + +/* +#cgo pkg-config: python-3.12-embed +#include +*/ +import "C" + +import ( + "unsafe" +) + +type Module struct { + Object +} + +func newModule(obj *C.PyObject) Module { + return Module{newObject(obj)} +} + +func ImportModule(name string) Module { + mod := C.PyImport_ImportModule(AllocCStr(name)) + return newModule(mod) +} + +func (m Module) Dict() Dict { + return newDict(C.PyModule_GetDict(m.obj)) +} + +func (m Module) AddObject(name string, obj Object) int { + return int(C.PyModule_AddObject(m.obj, AllocCStr(name), obj.obj)) +} + +func (m Module) AddFunction(name string, fn unsafe.Pointer, doc string) Func { + def := &C.PyMethodDef{ + ml_name: AllocCStr(name), + ml_meth: C.PyCFunction(fn), + ml_flags: C.METH_VARARGS, + ml_doc: AllocCStr(doc), + } + pyFn := C.PyCMethod_New(def, nil, m.obj, nil) + return newFunc(pyFn) +} diff --git a/object.go b/object.go new file mode 100644 index 0000000..17ba423 --- /dev/null +++ b/object.go @@ -0,0 +1,359 @@ +package python + +/* +#cgo pkg-config: python-3.12-embed +#include +*/ +import "C" + +import ( + "fmt" + "reflect" + "runtime" +) + +// pyObject is a wrapper type that holds a Python Object and automatically calls +// the Python Object's DecRef method during garbage collection. +type pyObject struct { + obj *C.PyObject +} + +func (obj *pyObject) Obj() *C.PyObject { + if obj == nil { + return nil + } + return obj.obj +} + +func (obj *pyObject) Nil() bool { + return obj == nil +} + +func (obj *pyObject) Ensure() { + if obj == nil { + C.PyErr_Print() + panic("nil Python object") + } +} + +// ---------------------------------------------------------------------------- + +type Object struct { + *pyObject +} + +func FromPy(obj *PyObject) Object { + return newObject(obj) +} + +func (obj Object) object() Object { + return obj +} + +func newObject(obj *C.PyObject) Object { + if obj == nil { + C.PyErr_Print() + panic("nil Python object") + } + o := &pyObject{obj: obj} + p := Object{o} + runtime.SetFinalizer(o, func(o *pyObject) { + C.Py_DecRef(o.obj) + }) + return p +} + +func (obj Object) GetAttr(name string) Object { + o := C.PyObject_GetAttrString(obj.obj, AllocCStr(name)) + C.Py_IncRef(o) + return newObject(o) +} + +func (obj Object) GetFloatAttr(name string) Float { + return obj.GetAttr(name).AsFloat() +} + +func (obj Object) GetLongAttr(name string) Long { + return obj.GetAttr(name).AsLong() +} + +func (obj Object) GetStrAttr(name string) Str { + return obj.GetAttr(name).AsStr() +} + +func (obj Object) GetBytesAttr(name string) Bytes { + return obj.GetAttr(name).AsBytes() +} + +func (obj Object) GetBoolAttr(name string) Bool { + return obj.GetAttr(name).AsBool() +} + +func (obj Object) GetDictAttr(name string) Dict { + return obj.GetAttr(name).AsDict() +} + +func (obj Object) GetListAttr(name string) List { + return obj.GetAttr(name).AsList() +} + +func (obj Object) GetTupleAttr(name string) Tuple { + return obj.GetAttr(name).AsTuple() +} + +func (obj Object) GetFuncAttr(name string) Func { + return obj.GetAttr(name).AsFunc() +} + +func (obj Object) AsFloat() Float { + return Cast[Float](obj) +} + +func (obj Object) AsLong() Long { + return Cast[Long](obj) +} + +func (obj Object) AsComplex() Complex { + return Cast[Complex](obj) +} + +func (obj Object) AsStr() Str { + return Cast[Str](obj) +} + +func (obj Object) AsBytes() Bytes { + return Cast[Bytes](obj) +} + +func (obj Object) AsBool() Bool { + return Cast[Bool](obj) +} + +func (obj Object) AsDict() Dict { + return Cast[Dict](obj) +} + +func (obj Object) AsList() List { + return Cast[List](obj) +} + +func (obj Object) AsTuple() Tuple { + return Cast[Tuple](obj) +} + +func (obj Object) AsFunc() Func { + return Cast[Func](obj) +} + +func (obj Object) AsModule() Module { + return Cast[Module](obj) +} + +func (obj Object) CallKeywords(name string, args ...any) func(kw any) Object { + return func(kw any) Object { + fn := Cast[Func](obj.GetAttr(name)) + pyArgs := MakeTuple(args...) + pyKw := Cast[Dict](From(kw)) + r := fn.call(pyArgs, pyKw) + return r + } +} + +func (obj Object) Call(name string, args ...any) Object { + fn := Cast[Func](obj.GetAttr(name)) + callArgs := MakeTuple(args...) + return fn.CallObject(callArgs) +} + +func (obj Object) Repr() string { + return newStr(C.PyObject_Repr(obj.obj)).String() +} + +func (obj Object) String() string { + return newStr(C.PyObject_Str(obj.obj)).String() +} + +func (obj Object) Obj() *C.PyObject { + if obj.Nil() { + return nil + } + return obj.pyObject.obj +} + +func From(v any) Object { + switch v := v.(type) { + case Objecter: + return newObject(v.Obj()) + case int8: + return newObject(C.PyLong_FromLong(C.long(v))) + case int16: + return newObject(C.PyLong_FromLong(C.long(v))) + case int32: + return newObject(C.PyLong_FromLong(C.long(v))) + case int64: + return newObject(C.PyLong_FromLongLong(C.longlong(v))) + case int: + return newObject(C.PyLong_FromLong(C.long(v))) + case uint8: + return newObject(C.PyLong_FromLong(C.long(v))) + case uint16: + return newObject(C.PyLong_FromLong(C.long(v))) + case uint32: + return newObject(C.PyLong_FromLong(C.long(v))) + case uint64: + return newObject(C.PyLong_FromUnsignedLongLong(C.ulonglong(v))) + case uint: + return newObject(C.PyLong_FromUnsignedLong(C.ulong(v))) + case float64: + return newObject(C.PyFloat_FromDouble(C.double(v))) + case string: + return newObject(C.PyUnicode_FromString(AllocCStr(v))) + case complex128: + return MakeComplex(v).Object + case complex64: + return MakeComplex(complex128(v)).Object + case []byte: + return MakeBytes(v).Object + case bool: + if v { + return True().Object + } else { + return False().Object + } + case *C.PyObject: + return newObject(v) + default: + vv := reflect.ValueOf(v) + switch vv.Kind() { + case reflect.Slice: + return fromSlice(vv).Object + } + panic(fmt.Errorf("unsupported type for Python: %T\n", v)) + } +} + +func ToValue(obj Object, v reflect.Value) { + // Handle nil pointer + if !v.IsValid() || !v.CanSet() { + return + } + + switch v.Kind() { + case reflect.Int8: + v.SetInt(Cast[Long](obj).Int64()) + case reflect.Int16: + v.SetInt(Cast[Long](obj).Int64()) + case reflect.Int32: + v.SetInt(Cast[Long](obj).Int64()) + case reflect.Int64: + v.SetInt(Cast[Long](obj).Int64()) + case reflect.Int: + v.SetInt(Cast[Long](obj).Int64()) + case reflect.Uint8: + v.SetUint(Cast[Long](obj).Uint64()) + case reflect.Uint16: + v.SetUint(Cast[Long](obj).Uint64()) + case reflect.Uint32: + v.SetUint(Cast[Long](obj).Uint64()) + case reflect.Uint64: + v.SetUint(Cast[Long](obj).Uint64()) + case reflect.Uint: + v.SetUint(Cast[Long](obj).Uint64()) + case reflect.Float32: + v.SetFloat(Cast[Float](obj).Float64()) + case reflect.Float64: + v.SetFloat(Cast[Float](obj).Float64()) + case reflect.Complex64, reflect.Complex128: + v.SetComplex(Cast[Complex](obj).Complex128()) + case reflect.String: + v.SetString(Cast[Str](obj).String()) + case reflect.Bool: + v.SetBool(Cast[Bool](obj).Bool()) + case reflect.Slice: + if v.Type().Elem().Kind() == reflect.Uint8 { // []byte + v.SetBytes(Cast[Bytes](obj).Bytes()) + } else { + list := Cast[List](obj) + l := list.Len() + slice := reflect.MakeSlice(v.Type(), l, l) + for i := 0; i < l; i++ { + item := list.GetItem(i) + ToValue(item, slice.Index(i)) + } + v.Set(slice) + } + default: + panic(fmt.Errorf("unsupported type conversion from Python object to %v", v.Type())) + } +} + +func To[T any](obj Object) (ret T) { + switch any(ret).(type) { + case int8: + return any(int8(Cast[Long](obj).Int64())).(T) + case int16: + return any(int16(Cast[Long](obj).Int64())).(T) + case int32: + return any(int32(Cast[Long](obj).Int64())).(T) + case int64: + return any(Cast[Long](obj).Int64()).(T) + case int: + return any(int(Cast[Long](obj).Int64())).(T) + case uint8: + return any(uint8(Cast[Long](obj).Uint64())).(T) + case uint16: + return any(uint16(Cast[Long](obj).Uint64())).(T) + case uint32: + return any(uint32(Cast[Long](obj).Uint64())).(T) + case uint64: + return any(Cast[Long](obj).Uint64()).(T) + case uint: + return any(uint(Cast[Long](obj).Uint64())).(T) + case float32: + return any(float32(Cast[Float](obj).Float64())).(T) + case float64: + return any(Cast[Float](obj).Float64()).(T) + case complex64: + return any(complex64(Cast[Complex](obj).Complex128())).(T) + case complex128: + return any(Cast[Complex](obj).Complex128()).(T) + case string: + return any(Cast[Str](obj).String()).(T) + case bool: + return any(Cast[Bool](obj).Bool()).(T) + case []byte: + return any(Cast[Bytes](obj).Bytes()).(T) + default: + v := reflect.ValueOf(ret) + switch v.Kind() { + case reflect.Slice: + return toSlice[T](obj, v) + } + panic(fmt.Errorf("unsupported type conversion from Python object to %T", ret)) + } +} + +func toSlice[T any](obj Object, v reflect.Value) T { + list := Cast[List](obj) + l := list.Len() + v = reflect.MakeSlice(v.Type(), l, l) + for i := 0; i < l; i++ { + v.Index(i).Set(reflect.ValueOf(To[T](list.GetItem(i)))) + } + return v.Interface().(T) +} + +func fromSlice(v reflect.Value) List { + l := v.Len() + list := newList(C.PyList_New(C.Py_ssize_t(l))) + for i := 0; i < l; i++ { + list.SetItem(i, From(v.Index(i).Interface())) + } + return list +} + +func (obj Object) CallMethod(name string, args ...any) Object { + mthd := Cast[Func](obj.GetAttr(name)) + argsTuple := MakeTuple(args...) + return mthd.CallObject(argsTuple) +} diff --git a/python.go b/python.go new file mode 100644 index 0000000..9ea94a7 --- /dev/null +++ b/python.go @@ -0,0 +1,96 @@ +package python + +/* +#cgo pkg-config: python-3.12-embed +#include +*/ +import "C" +import "unsafe" + +type PyObject = C.PyObject + +func Initialize() { + C.Py_Initialize() +} + +func Finalize() { + C.Py_FinalizeEx() +} + +// ---------------------------------------------------------------------------- + +func SetProgramName(name string) { + C.Py_SetProgramName(AllocWCStr(name)) +} + +type InputType = C.int + +const ( + SingleInput InputType = C.Py_single_input + FileInput InputType = C.Py_file_input + EvalInput InputType = C.Py_eval_input +) + +func CompileString(code, filename string, start InputType) Object { + return newObject(C.Py_CompileString(AllocCStr(code), AllocCStr(filename), C.int(start))) +} + +func EvalCode(code Object, globals, locals Dict) Object { + return newObject(C.PyEval_EvalCode(code.Obj(), globals.Obj(), locals.Obj())) +} + +// ---------------------------------------------------------------------------- + +// llgo:link Cast llgo.staticCast +func Cast[U, T Objecter](obj T) (u U) { + *(*T)(unsafe.Pointer(&u)) = obj + return +} + +// ---------------------------------------------------------------------------- + +func With[T Objecter](obj T, fn func(v T)) T { + obj.object().Call("__enter__") + defer obj.object().Call("__exit__") + fn(obj) + return obj +} + +// ---------------------------------------------------------------------------- + +var mainMod Module + +func MainModule() Module { + if mainMod.Nil() { + mainMod = ImportModule("__main__") + } + return mainMod +} + +var noneObj Object + +/* +from Dojo: +if self.none_value.is_null(): + + var list_obj = self.PyList_New(0) + var tuple_obj = self.PyTuple_New(0) + var callable_obj = self.PyObject_GetAttrString(list_obj, "reverse") + self.none_value = self.PyObject_CallObject(callable_obj, tuple_obj) + self.Py_DecRef(tuple_obj) + self.Py_DecRef(callable_obj) + self.Py_DecRef(list_obj) +*/ +func None() Object { + if noneObj.Nil() { + listObj := MakeList() + tupleObj := MakeTuple() + callableObj := listObj.GetFuncAttr("reverse") + noneObj = callableObj.CallObject(tupleObj) + } + return noneObj +} + +func Nil() Object { + return Object{} +} diff --git a/tuple.go b/tuple.go new file mode 100644 index 0000000..a46731e --- /dev/null +++ b/tuple.go @@ -0,0 +1,114 @@ +package python + +/* +#cgo pkg-config: python-3.12-embed +#include +*/ +import "C" + +type Tuple struct { + Object +} + +func newTuple(obj *C.PyObject) Tuple { + return Tuple{newObject(obj)} +} + +func MakeTupleWithLen(len int) Tuple { + return newTuple(C.PyTuple_New(C.Py_ssize_t(len))) +} + +func MakeTuple(args ...any) Tuple { + tuple := newTuple(C.PyTuple_New(C.Py_ssize_t(len(args)))) + for i, arg := range args { + obj := From(arg) + tuple.Set(i, obj) + } + return tuple +} + +func (t Tuple) Get(index int) Object { + v := C.PyTuple_GetItem(t.obj, C.Py_ssize_t(index)) + C.Py_IncRef(v) + return newObject(v) +} + +func (t Tuple) Set(index int, obj Objecter) { + C.PyTuple_SetItem(t.obj, C.Py_ssize_t(index), obj.Obj()) +} + +func (t Tuple) Len() int { + return int(C.PyTuple_Size(t.obj)) +} + +func (t Tuple) Slice(low, high int) Tuple { + return newTuple(C.PyTuple_GetSlice(t.obj, C.Py_ssize_t(low), C.Py_ssize_t(high))) +} + +func (t Tuple) ParseArgs(addrs ...any) bool { + if len(addrs) > t.Len() { + return false + } + + for i, addr := range addrs { + obj := t.Get(i) + + switch v := addr.(type) { + // 整数类型 + case *int: + *v = int(obj.AsLong().Int64()) + case *int8: + *v = int8(obj.AsLong().Int64()) + case *int16: + *v = int16(obj.AsLong().Int64()) + case *int32: + *v = int32(obj.AsLong().Int64()) + case *int64: + *v = obj.AsLong().Int64() + case *uint: + *v = uint(obj.AsLong().Int64()) + case *uint8: + *v = uint8(obj.AsLong().Int64()) + case *uint16: + *v = uint16(obj.AsLong().Int64()) + case *uint32: + *v = uint32(obj.AsLong().Int64()) + case *uint64: + *v = uint64(obj.AsLong().Int64()) + + // 浮点类型 + case *float32: + *v = float32(obj.AsFloat().Float64()) + case *float64: + *v = obj.AsFloat().Float64() + + // 复数类型 + case *complex64: + *v = complex64(obj.AsComplex().Complex128()) + case *complex128: + *v = obj.AsComplex().Complex128() + + // 字符串类型 + case *string: + *v = obj.AsStr().String() + case *[]byte: + *v = []byte(obj.AsStr().String()) + + // 布尔类型 + case *bool: + *v = obj.AsBool().Bool() + + case **PyObject: + *v = obj.Obj() + + // Python 对象 + case *Object: + *v = obj + + default: + return false + } + } + + return true +} diff --git a/unicode.go b/unicode.go new file mode 100644 index 0000000..4c09daf --- /dev/null +++ b/unicode.go @@ -0,0 +1,42 @@ +package python + +/* +#cgo pkg-config: python-3.12-embed +#include +*/ +import "C" +import ( + "reflect" + "unsafe" +) + +type Str struct { + Object +} + +func newStr(obj *C.PyObject) Str { + return Str{newObject(obj)} +} + +func MakeStr(s string) Str { + hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) + ptr := (*C.char)(unsafe.Pointer(hdr.Data)) + length := C.long(hdr.Len) + return newStr(C.PyUnicode_FromStringAndSize(ptr, length)) +} + +func (s Str) String() string { + var l C.long + buf := C.PyUnicode_AsUTF8AndSize(s.obj, &l) + return GoStringN((*C.char)(buf), int(l)) +} + +func (s Str) Len() int { + var l C.long + C.PyUnicode_AsUTF8AndSize(s.obj, &l) + return int(l) +} + +func (s Str) Encode(encoding string) Bytes { + return Cast[Bytes](s.CallMethod("encode", MakeStr(encoding))) +}