├── .gitignore ├── .pydevproject ├── LICENSE ├── README.md ├── common.go ├── concurrency.go ├── examples └── hello-world │ ├── api │ ├── functions.go │ ├── go_.go │ ├── module.go │ └── py_.go │ ├── foo.py │ └── main.go ├── exception.go ├── general.go ├── go.mod ├── go.sum ├── import.go ├── module.go ├── object.go ├── path.go ├── primitive.go ├── reference.go ├── scripts ├── _env ├── _functions ├── _trap ├── example ├── format └── refresh-mod └── type.go /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | __pycache__ 3 | 4 | dist/ 5 | -------------------------------------------------------------------------------- /.pydevproject: -------------------------------------------------------------------------------- 1 | 2 | 3 | Default 4 | python interpreter 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | py4go 2 | ===== 3 | 4 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/tliron/py4go)](https://goreportcard.com/report/github.com/tliron/py4go) 6 | 7 | Call Python 3 functions and methods from within your Go program while exposing Go functions and 8 | methods to Python. 9 | 10 | This is *not* an implementation of Python in Go. Rather, py4go works by embedding the CPython 11 | runtime into your Go program using [cgo](https://github.com/golang/go/wiki/cgo) functionality. 12 | 13 | The expected use cases are not low-latency integration, but rather *tight* bidirectional 14 | integration. You can combine the full Go ecosystem with the full Python ecosystem. 15 | 16 | Though you can achieve some integration by using Go's [exec](https://pkg.go.dev/os/exec) 17 | package to run `python`, with py4go you get fine-grained access to individual functions, objects, 18 | methods, and variables. 19 | 20 | To get started try running [`scripts/example`](scripts/example/). Note that you need the Python 21 | development libraries installed. E.g. in Fedora: 22 | 23 | sudo dnf install python3-devel 24 | 25 | 26 | Example Usage 27 | ------------- 28 | 29 | ```go 30 | package main 31 | 32 | import ( 33 | "fmt" 34 | "github.com/tliron/py4go" 35 | ) 36 | 37 | func main() { 38 | // Initialize Python 39 | python.Initialize() 40 | defer python.Finalize() 41 | 42 | // Import Python code (foo.py) 43 | foo, _ := python.Import("foo") 44 | defer foo.Release() 45 | 46 | // Get access to a Python function 47 | hello, _ := foo.GetAttr("hello") 48 | defer hello.Release() 49 | 50 | // Call the function with arguments 51 | r, _ := hello.Call("myargument") 52 | defer r.Release() 53 | fmt.Printf("Returned: %s\n", r.String()) 54 | 55 | // Expose a Go function to Python via a C wrapper 56 | // (Just use "import api" from Python) 57 | api, _ := python.CreateModule("api") 58 | defer api.Release() 59 | api.AddModuleCFunctionNoArgs("my_function", C.api_my_function) 60 | api.EnableModule() 61 | } 62 | ``` 63 | 64 | Calling Python code from Go is relatively straightforward because Python is a dynamic language and 65 | CPython is an interpreted runtime. Exposing Go code to Python is more involved as it requires writing 66 | wrapper functions in C, which we omitted in the example above. See the [examples](examples/) directory 67 | for more detail. 68 | 69 | In the future we are hoping to make this easier, perhaps via a C code generator based on static 70 | analysis of function signatures and types. 71 | 72 | 73 | Caveats 74 | ------- 75 | 76 | There are several issues to be aware of: 77 | 78 | * It's more difficult to distribute your Go program because you *must* have the CPython library 79 | available on the target operating system with a specific name. Because different operating systems 80 | have their own conventions for naming this library, to create a truly portable distribution it may 81 | be best to distribute your program as a packaged container, e.g. using Flatpak, Docker, or Snap. 82 | * It is similarly more difficult to *build* your Go program. We are using `pkg-config: python3-embed` to 83 | locate the CPython SDK, which works on Fedora-based operating systems. But, because where you 84 | *build* will determine the requirements for where you will *run*, it may be best to build on 85 | Fedora, either directly or in a virtual machine or container. Unfortunately cgo does not let us 86 | parameterize that `pkg-config` directive, thus you will have to modify our source files in order to 87 | build on/for other operating systems. 88 | * Calling functions and passing data between these two high-level language's runtime environments 89 | obviously incurs some overhead. Notably strings are sometimes copied multiple times internally, 90 | and may be encoded and decoded (Go normally uses UTF-8, Python defaults to UCS4). If you are 91 | frequently calling back and forth be aware of possible performance degradation. As always, if you 92 | experience a problem measure first and identify the bottleneck before prematurely optimizing! 93 | * Similarly, be aware that you are simultaneously running two memory management runtimes, each with 94 | its own heap allocation and garbage collection threads, and that Go is unaware of Python's. Your 95 | Go code will thus need to explicitly call `Release` on all Python references to ensure that they are 96 | garbage collected. Luckily, the `defer` keyword makes this easy enough in many circumstances. 97 | * Concurrency is a bit tricky in Python due to its infamous Global Interpreter Lock (GIL). If 98 | you are calling Python code from a Goroutine make sure to call `python.SaveThreadState` and 99 | `python.EnsureGilState` as appropriate. See the examples for more detail. 100 | 101 | 102 | References 103 | ---------- 104 | 105 | * [go-python](https://github.com/sbinet/go-python) is a similar and more mature project for Python 106 | 2. 107 | * [goPy](https://github.com/qur/gopy) is a much older project for Python 2. 108 | * [gopy](https://github.com/go-python/gopy) generates Python wrappers for Go functions. 109 | * [setuptools-golang](https://github.com/asottile/setuptools-golang) allows you to include Go 110 | libraries in Python packages. 111 | -------------------------------------------------------------------------------- /common.go: -------------------------------------------------------------------------------- 1 | package python 2 | 3 | // See: 4 | // https://docs.python.org/3/c-api/ 5 | // https://github.com/golang/go/wiki/cgo 6 | // https://www.datadoghq.com/blog/engineering/cgo-and-python/ 7 | // https://github.com/sbinet/go-python 8 | 9 | // #cgo pkg-config: python3-embed 10 | // #cgo LDFLAGS: -lpython3 11 | import "C" 12 | -------------------------------------------------------------------------------- /concurrency.go: -------------------------------------------------------------------------------- 1 | package python 2 | 3 | // See: 4 | // https://docs.python.org/3/c-api/init.html 5 | 6 | /* 7 | #define PY_SSIZE_T_CLEAN 8 | #include 9 | */ 10 | import "C" 11 | 12 | // 13 | // ThreadState 14 | // 15 | 16 | type ThreadState struct { 17 | State *C.PyThreadState 18 | } 19 | 20 | func SaveThreadState() *ThreadState { 21 | return &ThreadState{C.PyEval_SaveThread()} 22 | } 23 | 24 | func (self *ThreadState) Restore() { 25 | C.PyEval_RestoreThread(self.State) 26 | } 27 | 28 | // 29 | // GilState 30 | // 31 | 32 | type GilState struct { 33 | State C.PyGILState_STATE 34 | } 35 | 36 | func EnsureGilState() *GilState { 37 | return &GilState{C.PyGILState_Ensure()} 38 | } 39 | 40 | func (self *GilState) Release() { 41 | C.PyGILState_Release(self.State) 42 | } 43 | -------------------------------------------------------------------------------- /examples/hello-world/api/functions.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // Here we define our functions in plain Go 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | func sayGoodbye() { 10 | fmt.Println("Go >> Goodbye from Go!") 11 | } 12 | 13 | func concat(a string, b string) string { 14 | fmt.Printf("Go >> Concatenating %q and %q\n", a, b) 15 | return a + " " + b 16 | } 17 | -------------------------------------------------------------------------------- /examples/hello-world/api/go_.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // Here we export cgo wrappers for our plain Go functions 4 | // They handle the conversion between Go and C types 5 | 6 | // Note: cgo exports cannot be in the same file as cgo preamble funtions, 7 | // which is why this file cannot be combined with "py_.go" 8 | 9 | import "C" 10 | 11 | //export go_api_sayGoodbye 12 | func go_api_sayGoodbye() { 13 | // Note that we could have just exported sayGoodbye directly because it has no arguments, 14 | // and thus nothing to convert, But for completion we are adding this straightforward 15 | // wrapper 16 | sayGoodbye() 17 | } 18 | 19 | //export go_api_concat 20 | func go_api_concat(a *C.char, b *C.char) *C.char { 21 | return C.CString(concat(C.GoString(a), C.GoString(b))) 22 | } 23 | -------------------------------------------------------------------------------- /examples/hello-world/api/module.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // Here we add our "py_" C functions to the module 4 | 5 | // Note that this file could have been combined with "py_.go", but we preferred a clean separation 6 | // The cost is that we must forward-declare the "py_" functions in the cgo preamble here 7 | 8 | import ( 9 | python "github.com/tliron/py4go" 10 | ) 11 | 12 | /* 13 | #cgo pkg-config: python3-embed 14 | 15 | #define PY_SSIZE_T_CLEAN 16 | #include 17 | 18 | PyObject *py_api_sayGoodbye(PyObject *self, PyObject *unused); 19 | PyObject *py_api_concat(PyObject *self, PyObject *args); 20 | PyObject *py_api_concat_fast(PyObject *self, PyObject **args, Py_ssize_t nargs); 21 | */ 22 | import "C" 23 | 24 | func CreateModule() (*python.Reference, error) { 25 | if module, err := python.CreateModule("api"); err == nil { 26 | if err := module.AddModuleCFunctionNoArgs("say_goodbye", C.py_api_sayGoodbye); err != nil { 27 | module.Release() 28 | return nil, err 29 | } 30 | 31 | if err := module.AddModuleCFunctionArgs("concat", C.py_api_concat); err != nil { 32 | module.Release() 33 | return nil, err 34 | } 35 | 36 | if err := module.AddModuleCFunctionFastArgs("concat_fast", C.py_api_concat_fast); err != nil { 37 | module.Release() 38 | return nil, err 39 | } 40 | 41 | return module, nil 42 | } else { 43 | return nil, err 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/hello-world/api/py_.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // Here we define Python wrappers in C for our "go_" functions 4 | // They handle the conversion between Python and C types 5 | 6 | // We are demonstrating the two supported Python ABIs: 7 | // 1) the more common PyCFunction ABI, which passes arguments as Python tuples and dicts, and 8 | // 2) the newer _PyCFunctionFast ABI, which passes arguments as a more efficient stack 9 | 10 | // Unfortunately we must use C and not Go to write these functions because: 11 | // 1) the "PyArg_Parse_" functions all use variadic arguments, which are not supported by cgo, and 12 | // 2) the "PyArg_Parse_" functions unpack arguments to pointers, which we cannot implement in Go 13 | 14 | // Note: cgo exports cannot be in the same file as cgo preamble functions, 15 | // which is why this file cannot be combined with "go_.go" 16 | // and is also why must forward-declare the "go_" functions in the cgo preamble here 17 | 18 | // See: 19 | // https://docs.python.org/3/c-api/arg.html 20 | 21 | /* 22 | #cgo pkg-config: python3-embed 23 | 24 | #define PY_SSIZE_T_CLEAN 25 | #include 26 | 27 | void go_api_sayGoodbye(); 28 | char *go_api_concat(char*, char*); 29 | 30 | // PyCFunction signature 31 | PyObject *py_api_sayGoodbye(PyObject *self, PyObject *unused) { 32 | go_api_sayGoodbye(); 33 | return Py_None; 34 | } 35 | 36 | // PyCFunction signature 37 | PyObject *py_api_concat(PyObject *self, PyObject *args) { 38 | char *arg1 = NULL, *arg2 = NULL; 39 | PyArg_ParseTuple(args, "ss", &arg1, &arg2); 40 | char *r = go_api_concat(arg1, arg2); 41 | return PyUnicode_FromString(r); 42 | } 43 | 44 | // _PyCFunctionFast signature 45 | PyObject *py_api_concat_fast(PyObject *self, PyObject **args, Py_ssize_t nargs) { 46 | char *arg1 = NULL, *arg2 = NULL; 47 | _PyArg_ParseStack(args, nargs, "ss", &arg1, &arg2); 48 | char *r = go_api_concat(arg1, arg2); 49 | return PyUnicode_FromString(r); 50 | } 51 | */ 52 | import "C" 53 | -------------------------------------------------------------------------------- /examples/hello-world/foo.py: -------------------------------------------------------------------------------- 1 | import api 2 | 3 | def hello(name): 4 | print("Python >> Hello, " + name) 5 | return "You are " + name 6 | 7 | def goodbye(): 8 | print("Python >> Calling Go functions") 9 | api.say_goodbye() 10 | 11 | def bad(): 12 | raise Exception("this is a Python exception") 13 | 14 | def say_name(): 15 | print("Python >> The name is " + api.concat("Tal", "Liron")) 16 | 17 | def say_name_fast(): 18 | print("Python >> The name is " + api.concat_fast("Tal", "Liron")) 19 | 20 | size = 0 21 | 22 | def grow(count): 23 | global size 24 | size += count 25 | 26 | class Person: 27 | def __init__(self, name): 28 | self.name = name 29 | 30 | def greet(self): 31 | print("Python >> Greetings, " + self.name) 32 | 33 | person = Person("Linus") 34 | -------------------------------------------------------------------------------- /examples/hello-world/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | python "github.com/tliron/py4go" 8 | "github.com/tliron/py4go/examples/hello-world/api" 9 | ) 10 | 11 | func version() { 12 | fmt.Printf("Go >> Python version:\n%s\n", python.Version()) 13 | } 14 | 15 | func typeChecking() { 16 | fmt.Println("Go >> Type checking:") 17 | float, _ := python.NewPrimitiveReference(1.0) 18 | fmt.Printf("Go >> IsFloat: %t\n", float.IsFloat()) 19 | } 20 | 21 | func callPythonFunction(module *python.Reference) { 22 | fmt.Println("Go >> Calling a Python function:") 23 | 24 | hello, _ := module.GetAttr("hello") 25 | defer hello.Release() 26 | 27 | r, _ := hello.Call("Tal") 28 | defer r.Release() 29 | 30 | r_, _ := r.ToString() 31 | fmt.Printf("Go >> Python function returned: %s\n", r_) 32 | } 33 | 34 | func callPythonMethod(module *python.Reference) { 35 | fmt.Println("Go >> Calling a Python method:") 36 | 37 | person, _ := module.GetAttr("person") 38 | defer person.Release() 39 | 40 | greet, _ := person.GetAttr("greet") 41 | defer greet.Release() 42 | 43 | greet.Call() 44 | } 45 | 46 | func getPythonException(module *python.Reference) { 47 | fmt.Println("Go >> Python exception as Go error:") 48 | 49 | bad, _ := module.GetAttr("bad") 50 | defer bad.Release() 51 | 52 | if _, err := bad.Call(); err != nil { 53 | fmt.Printf("Go >> Error message: %s\n", err) 54 | } 55 | } 56 | 57 | func callGoFromPython(module *python.Reference) { 58 | goodbye, _ := module.GetAttr("goodbye") 59 | defer goodbye.Release() 60 | goodbye.Call() 61 | 62 | sayName, _ := module.GetAttr("say_name") 63 | defer sayName.Release() 64 | sayName.Call() 65 | 66 | sayNameFast, _ := module.GetAttr("say_name_fast") 67 | defer sayNameFast.Release() 68 | sayNameFast.Call() 69 | } 70 | 71 | func concurrency(module *python.Reference) { 72 | fmt.Println("Go >> Concurrency:") 73 | 74 | grow, _ := module.GetAttr("grow") 75 | defer grow.Release() 76 | 77 | func() { 78 | // Release Python's lock on our main thread, allowing other threads to execute 79 | // (Without this our calls to "grow", from other threads, will block forever) 80 | threadState := python.SaveThreadState() 81 | defer threadState.Restore() 82 | 83 | // Parallel work: 84 | 85 | var waitGroup sync.WaitGroup 86 | defer waitGroup.Wait() 87 | 88 | for i := 0; i < 5; i++ { 89 | waitGroup.Add(1) 90 | 91 | go func() { 92 | defer waitGroup.Done() 93 | 94 | for i := 0; i < 100; i++ { 95 | // We must manually acquire Python's Global Interpreter Lock (GIL), 96 | // because Python doesn't know about our Go "threads" (goroutines) 97 | gs := python.EnsureGilState() 98 | defer gs.Release() 99 | 100 | // (Note: We could also have acquired the GIL outside of this for-loop; 101 | // it's up to how we want to balance concurrency with the cost of context 102 | // switching) 103 | 104 | grow.Call(1) 105 | } 106 | }() 107 | } 108 | }() 109 | 110 | size, _ := module.GetAttr("size") 111 | defer size.Release() 112 | 113 | size_, _ := size.ToInt64() 114 | fmt.Printf("Go >> Size is %d\n", size_) 115 | } 116 | 117 | func main() { 118 | python.PrependPythonPath(".") 119 | 120 | python.Initialize() 121 | defer python.Finalize() 122 | 123 | version() 124 | fmt.Println() 125 | 126 | typeChecking() 127 | fmt.Println() 128 | 129 | api, _ := api.CreateModule() 130 | defer api.Release() 131 | api.EnableModule() 132 | 133 | foo, _ := python.Import("foo") 134 | defer foo.Release() 135 | 136 | callPythonFunction(foo) 137 | fmt.Println() 138 | 139 | callPythonMethod(foo) 140 | fmt.Println() 141 | 142 | getPythonException(foo) 143 | fmt.Println() 144 | 145 | callGoFromPython(foo) 146 | fmt.Println() 147 | 148 | concurrency(foo) 149 | } 150 | -------------------------------------------------------------------------------- /exception.go: -------------------------------------------------------------------------------- 1 | package python 2 | 3 | // See: 4 | // https://docs.python.org/3/c-api/exceptions.html 5 | 6 | import ( 7 | "errors" 8 | ) 9 | 10 | /* 11 | #define PY_SSIZE_T_CLEAN 12 | #include 13 | */ 14 | import "C" 15 | 16 | func HasException() bool { 17 | return C.PyErr_Occurred() != nil 18 | } 19 | 20 | func GetError() error { 21 | if exception := FetchException(); exception != nil { 22 | return exception 23 | } else { 24 | return errors.New("Python error without an exception") 25 | } 26 | } 27 | 28 | // 29 | // Exception 30 | // 31 | 32 | type Exception struct { 33 | Type *Reference 34 | Value *Reference 35 | Traceback *Reference 36 | } 37 | 38 | func FetchException() *Exception { 39 | var type_, value, traceback *C.PyObject 40 | C.PyErr_Fetch(&type_, &value, &traceback) 41 | if type_ != nil { 42 | defer C.PyErr_Restore(type_, value, traceback) 43 | 44 | var type__, value_, traceback_ *Reference 45 | 46 | if type_ != nil { 47 | type__ = NewReference(type_) 48 | } 49 | 50 | if value != nil { 51 | value_ = NewReference(value) 52 | } 53 | 54 | if traceback != nil { 55 | traceback_ = NewReference(traceback) 56 | } 57 | 58 | return NewExceptionRaw(type__, value_, traceback_) 59 | } else { 60 | return nil 61 | } 62 | } 63 | 64 | func NewExceptionRaw(type_ *Reference, value *Reference, traceback *Reference) *Exception { 65 | return &Exception{ 66 | Type: type_, 67 | Value: value, 68 | Traceback: traceback, 69 | } 70 | } 71 | 72 | // error signature 73 | func (self *Exception) Error() string { 74 | // TODO: include traceback? 75 | if self.Value != nil { 76 | return self.Value.String() 77 | } else if self.Type != nil { 78 | return self.Type.String() 79 | } else { 80 | return "malformed Python exception" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /general.go: -------------------------------------------------------------------------------- 1 | package python 2 | 3 | // See: 4 | // https://docs.python.org/3/c-api/init.html 5 | 6 | /* 7 | #define PY_SSIZE_T_CLEAN 8 | #include 9 | */ 10 | import "C" 11 | 12 | func Initialize() { 13 | C.Py_Initialize() 14 | } 15 | 16 | func Finalize() error { 17 | if C.Py_FinalizeEx() == 0 { 18 | return nil 19 | } else { 20 | return GetError() 21 | } 22 | } 23 | 24 | func Version() string { 25 | return C.GoString(C.Py_GetVersion()) 26 | } 27 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tliron/py4go 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tliron/py4go/2a45aeb39bc7dbdc5653026800fd21ad122c529f/go.sum -------------------------------------------------------------------------------- /import.go: -------------------------------------------------------------------------------- 1 | package python 2 | 3 | // See: 4 | // https://docs.python.org/3/c-api/import.html 5 | 6 | import ( 7 | "unsafe" 8 | ) 9 | 10 | /* 11 | #define PY_SSIZE_T_CLEAN 12 | #include 13 | */ 14 | import "C" 15 | 16 | func Import(name string) (*Reference, error) { 17 | name_ := C.CString(name) 18 | defer C.free(unsafe.Pointer(name_)) 19 | 20 | if import_ := C.PyImport_ImportModule(name_); import_ != nil { 21 | return NewReference(import_), nil 22 | } else { 23 | return nil, GetError() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /module.go: -------------------------------------------------------------------------------- 1 | package python 2 | 3 | import ( 4 | "unsafe" 5 | ) 6 | 7 | /* 8 | #define PY_SSIZE_T_CLEAN 9 | #include 10 | */ 11 | import "C" 12 | 13 | var ModuleType = NewType(&C.PyModule_Type) 14 | 15 | func CreateModule(name string) (*Reference, error) { 16 | name_ := C.CString(name) 17 | defer C.free(unsafe.Pointer(name_)) 18 | 19 | var definition C.PyModuleDef 20 | definition.m_name = name_ 21 | 22 | if module := C.PyModule_Create2(&definition, C.PYTHON_ABI_VERSION); module != nil { 23 | return NewReference(module), nil 24 | } else { 25 | return nil, GetError() 26 | } 27 | } 28 | 29 | func NewModuleRaw(name string) (*Reference, error) { 30 | name_ := C.CString(name) 31 | defer C.free(unsafe.Pointer(name_)) 32 | 33 | if module := C.PyModule_New(name_); module != nil { 34 | return NewReference(module), nil 35 | } else { 36 | return nil, GetError() 37 | } 38 | } 39 | 40 | func (self *Reference) GetModuleName() (string, error) { 41 | if name := C.PyModule_GetName(self.Object); name != nil { 42 | defer C.free(unsafe.Pointer(name)) // TODO: need this? 43 | 44 | return C.GoString(name), nil 45 | } else { 46 | return "", GetError() 47 | } 48 | } 49 | 50 | func (self *Reference) EnableModule() error { 51 | if name := C.PyModule_GetNameObject(self.Object); name != nil { 52 | name_ := NewReference(name) 53 | defer name_.Release() 54 | 55 | if moduleDict := C.PyImport_GetModuleDict(); moduleDict != nil { 56 | moduleDict_ := NewReference(moduleDict) 57 | defer moduleDict_.Release() 58 | 59 | return moduleDict_.SetDictItem(name_, self) 60 | } else { 61 | return GetError() 62 | } 63 | } else { 64 | return GetError() 65 | } 66 | } 67 | 68 | // PyCFunction signature, second argument unused 69 | func (self *Reference) AddModuleCFunctionNoArgs(name string, function unsafe.Pointer) error { 70 | return self.addModuleCFunction(name, function, C.METH_NOARGS) 71 | } 72 | 73 | // PyCFunction signature, second argument is the Python argument 74 | func (self *Reference) AddModuleCFunctionOneArg(name string, function unsafe.Pointer) error { 75 | return self.addModuleCFunction(name, function, C.METH_O) 76 | } 77 | 78 | // PyCFunction signature, second argument is tuple of Python arguments 79 | func (self *Reference) AddModuleCFunctionArgs(name string, function unsafe.Pointer) error { 80 | return self.addModuleCFunction(name, function, C.METH_VARARGS) 81 | } 82 | 83 | // PyCFunctionWithKeywords signature 84 | func (self *Reference) AddModuleCFunctionArgsAndKeywords(name string, function unsafe.Pointer) error { 85 | return self.addModuleCFunction(name, function, C.METH_VARARGS|C.METH_KEYWORDS) 86 | } 87 | 88 | // _PyCFunctionFast signature 89 | func (self *Reference) AddModuleCFunctionFastArgs(name string, function unsafe.Pointer) error { 90 | return self.addModuleCFunction(name, function, C.METH_FASTCALL) 91 | } 92 | 93 | // _PyCFunctionFastWithKeywords signature 94 | func (self *Reference) AddModuleCFunctionFastArgsAndKeywords(name string, function unsafe.Pointer) error { 95 | return self.addModuleCFunction(name, function, C.METH_FASTCALL|C.METH_KEYWORDS) 96 | } 97 | 98 | func (self *Reference) addModuleCFunction(name string, function unsafe.Pointer, flags C.int) error { 99 | name_ := C.CString(name) 100 | defer C.free(unsafe.Pointer(name_)) 101 | 102 | methodDef := []C.PyMethodDef{ 103 | { 104 | ml_name: name_, 105 | ml_meth: C.PyCFunction(function), 106 | ml_flags: flags, 107 | }, 108 | {}, // NULL end of array 109 | } 110 | 111 | if C.PyModule_AddFunctions(self.Object, &methodDef[0]) == 0 { 112 | return nil 113 | } else { 114 | return GetError() 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /object.go: -------------------------------------------------------------------------------- 1 | package python 2 | 3 | // See: 4 | // https://docs.python.org/3/c-api/object.html 5 | 6 | import ( 7 | "unsafe" 8 | ) 9 | 10 | /* 11 | #define PY_SSIZE_T_CLEAN 12 | #include 13 | */ 14 | import "C" 15 | 16 | // fmt.Stringer interface 17 | func (self *Reference) String() string { 18 | if str := C.PyObject_Str(self.Object); str != nil { 19 | if data := C.PyUnicode_AsUTF8String(str); data != nil { 20 | defer C.Py_DecRef(data) 21 | if string_ := C.PyBytes_AsString(data); string_ != nil { 22 | return C.GoString(string_) 23 | } else { 24 | return "" 25 | } 26 | } else { 27 | return "" 28 | } 29 | } else { 30 | return "" 31 | } 32 | } 33 | 34 | func (self *Reference) Acquire() { 35 | C.Py_IncRef(self.Object) 36 | } 37 | 38 | func (self *Reference) Release() { 39 | C.Py_DecRef(self.Object) 40 | } 41 | 42 | func (self *Reference) Str() (*Reference, error) { 43 | if str := C.PyObject_Str(self.Object); str != nil { 44 | return NewReference(str), nil 45 | } else { 46 | return nil, GetError() 47 | } 48 | } 49 | 50 | func (self *Reference) GetAttr(name string) (*Reference, error) { 51 | name_ := C.CString(name) 52 | defer C.free(unsafe.Pointer(name_)) 53 | 54 | if attr := C.PyObject_GetAttrString(self.Object, name_); attr != nil { 55 | return NewReference(attr), nil 56 | } else { 57 | return nil, GetError() 58 | } 59 | } 60 | 61 | func (self *Reference) SetAttr(name string, reference *Reference) error { 62 | name_ := C.CString(name) 63 | defer C.free(unsafe.Pointer(name_)) 64 | 65 | if C.PyObject_SetAttrString(self.Object, name_, reference.Object) == 0 { 66 | return nil 67 | } else { 68 | return GetError() 69 | } 70 | } 71 | 72 | func (self *Reference) Call(args ...interface{}) (*Reference, error) { 73 | if args_, err := NewTuple(args...); err == nil { 74 | if kw, err := NewDict(); err == nil { 75 | return self.CallRaw(args_, kw) 76 | } else { 77 | return nil, err 78 | } 79 | } else { 80 | return nil, err 81 | } 82 | } 83 | 84 | func (self *Reference) CallRaw(args *Reference, kw *Reference) (*Reference, error) { 85 | if r := C.PyObject_Call(self.Object, args.Object, kw.Object); r != nil { 86 | return NewReference(r), nil 87 | } else { 88 | return nil, GetError() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /path.go: -------------------------------------------------------------------------------- 1 | package python 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | const PYTHONPATH = "PYTHONPATH" 9 | 10 | func SetPythonPath(path ...string) { 11 | path_ := filepath.Join(path...) 12 | os.Setenv(PYTHONPATH, path_) 13 | } 14 | 15 | func AppendPythonPath(path ...string) { 16 | path_ := filepath.SplitList(os.Getenv(PYTHONPATH)) 17 | path_ = append(path_, path...) 18 | path__ := filepath.Join(path_...) 19 | os.Setenv(PYTHONPATH, path__) 20 | } 21 | 22 | func PrependPythonPath(path ...string) { 23 | path_ := filepath.SplitList(os.Getenv(PYTHONPATH)) 24 | path_ = append(path, path_...) 25 | path__ := filepath.Join(path_...) 26 | os.Setenv(PYTHONPATH, path__) 27 | } 28 | -------------------------------------------------------------------------------- /primitive.go: -------------------------------------------------------------------------------- 1 | package python 2 | 3 | // See: 4 | // https://docs.python.org/3/c-api/concrete.html 5 | 6 | import ( 7 | "fmt" 8 | "unsafe" 9 | ) 10 | 11 | /* 12 | #include "stdint.h" 13 | #define PY_SSIZE_T_CLEAN 14 | #include 15 | */ 16 | import "C" 17 | 18 | func NewPrimitiveReference(value interface{}) (*Reference, error) { 19 | if value == nil { 20 | return None, nil 21 | } 22 | 23 | switch value_ := value.(type) { 24 | case *Reference: 25 | return value_, nil 26 | case bool: 27 | if value_ { 28 | return True, nil 29 | } else { 30 | return False, nil 31 | } 32 | case int64: 33 | return NewLong(value_) 34 | case int32: 35 | return NewLong(int64(value_)) 36 | case int8: 37 | return NewLong(int64(value_)) 38 | case int: 39 | return NewLong(int64(value_)) 40 | case float64: 41 | return NewFloat(value_) 42 | case float32: 43 | return NewFloat(float64(value_)) 44 | case string: 45 | return NewUnicode(value_) 46 | case []interface{}: 47 | return NewList(value_...) 48 | case []byte: 49 | return NewBytes(value_) 50 | } 51 | 52 | return nil, fmt.Errorf("unsupported primitive: %s", value) 53 | } 54 | 55 | // 56 | // None 57 | // 58 | 59 | var None = NewReference(C.Py_None) 60 | 61 | // 62 | // Bool 63 | // 64 | 65 | var BoolType = NewType(&C.PyBool_Type) 66 | 67 | var True = NewReference(C.Py_True) 68 | var False = NewReference(C.Py_False) 69 | 70 | func (self *Reference) IsBool() bool { 71 | return self.Type().IsSubtype(BoolType) 72 | } 73 | 74 | func (self *Reference) ToBool() bool { 75 | switch self.Object { 76 | case C.Py_True: 77 | return true 78 | case C.Py_False: 79 | return false 80 | } 81 | return false 82 | } 83 | 84 | // 85 | // Long 86 | // 87 | 88 | var LongType = NewType(&C.PyLong_Type) 89 | 90 | func NewLong(value int64) (*Reference, error) { 91 | if long := C.PyLong_FromLong(C.long(value)); long != nil { 92 | return NewReference(long), nil 93 | } else { 94 | return nil, GetError() 95 | } 96 | } 97 | 98 | func (self *Reference) IsLong() bool { 99 | // More efficient to use the flag 100 | return self.Type().HasFlag(C.Py_TPFLAGS_LONG_SUBCLASS) 101 | } 102 | 103 | func (self *Reference) ToInt64() (int64, error) { 104 | if long := C.PyLong_AsLong(self.Object); !HasException() { 105 | return int64(long), nil 106 | } else { 107 | return 0, GetError() 108 | } 109 | } 110 | 111 | // 112 | // Float 113 | // 114 | 115 | var FloatType = NewType(&C.PyFloat_Type) 116 | 117 | func NewFloat(value float64) (*Reference, error) { 118 | if float := C.PyFloat_FromDouble(C.double(value)); float != nil { 119 | return NewReference(float), nil 120 | } else { 121 | return nil, GetError() 122 | } 123 | } 124 | 125 | func (self *Reference) IsFloat() bool { 126 | return self.Type().IsSubtype(FloatType) 127 | } 128 | 129 | func (self *Reference) ToFloat64() (float64, error) { 130 | if double := C.PyFloat_AsDouble(self.Object); !HasException() { 131 | return float64(double), nil 132 | } else { 133 | return 0.0, GetError() 134 | } 135 | } 136 | 137 | // 138 | // Unicode 139 | // 140 | 141 | var UnicodeType = NewType(&C.PyUnicode_Type) 142 | 143 | func NewUnicode(value string) (*Reference, error) { 144 | value_ := C.CString(value) 145 | defer C.free(unsafe.Pointer(value_)) 146 | 147 | if unicode := C.PyUnicode_FromString(value_); unicode != nil { 148 | return NewReference(unicode), nil 149 | } else { 150 | return nil, GetError() 151 | } 152 | } 153 | 154 | func (self *Reference) IsUnicode() bool { 155 | // More efficient to use the flag 156 | return self.Type().HasFlag(C.Py_TPFLAGS_UNICODE_SUBCLASS) 157 | } 158 | 159 | func (self *Reference) ToString() (string, error) { 160 | if utf8stringBytes := C.PyUnicode_AsUTF8String(self.Object); utf8stringBytes != nil { 161 | defer C.Py_DecRef(utf8stringBytes) 162 | 163 | if utf8string := C.PyBytes_AsString(utf8stringBytes); utf8string != nil { 164 | return C.GoString(utf8string), nil 165 | } else { 166 | return "", GetError() 167 | } 168 | } else { 169 | return "", GetError() 170 | } 171 | } 172 | 173 | // 174 | // Tuple 175 | // 176 | 177 | var TupleType = NewType(&C.PyTuple_Type) 178 | 179 | func NewTuple(items ...interface{}) (*Reference, error) { 180 | if tuple, err := NewTupleRaw(len(items)); err == nil { 181 | for index, item := range items { 182 | if item_, err := NewPrimitiveReference(item); err == nil { 183 | if err := tuple.SetTupleItem(index, item_); err != nil { 184 | return nil, err 185 | } 186 | } else { 187 | return nil, err 188 | } 189 | } 190 | return tuple, nil 191 | } else { 192 | return nil, GetError() 193 | } 194 | } 195 | 196 | func NewTupleRaw(size int) (*Reference, error) { 197 | if tuple := C.PyTuple_New(C.int64_t(size)); tuple != nil { 198 | return NewReference(tuple), nil 199 | } else { 200 | return nil, GetError() 201 | } 202 | } 203 | 204 | func (self *Reference) IsTuple() bool { 205 | // More efficient to use the flag 206 | return self.Type().HasFlag(C.Py_TPFLAGS_TUPLE_SUBCLASS) 207 | } 208 | 209 | func (self *Reference) SetTupleItem(index int, item *Reference) error { 210 | if C.PyTuple_SetItem(self.Object, C.int64_t(index), item.Object) == 0 { 211 | return nil 212 | } else { 213 | return GetError() 214 | } 215 | } 216 | 217 | // 218 | // List 219 | // 220 | 221 | var ListType = NewType(&C.PyList_Type) 222 | 223 | func NewList(items ...interface{}) (*Reference, error) { 224 | if list, err := NewListRaw(len(items)); err == nil { 225 | for index, item := range items { 226 | if item_, err := NewPrimitiveReference(item); err == nil { 227 | if err := list.SetListItem(index, item_); err != nil { 228 | return nil, err 229 | } 230 | } else { 231 | return nil, err 232 | } 233 | } 234 | return list, nil 235 | } else { 236 | return nil, GetError() 237 | } 238 | } 239 | 240 | func NewListRaw(size int) (*Reference, error) { 241 | if list := C.PyList_New(C.int64_t(size)); list != nil { 242 | return NewReference(list), nil 243 | } else { 244 | return nil, GetError() 245 | } 246 | } 247 | 248 | func (self *Reference) IsList() bool { 249 | // More efficient to use the flag 250 | return self.Type().HasFlag(C.Py_TPFLAGS_LIST_SUBCLASS) 251 | } 252 | 253 | func (self *Reference) SetListItem(index int, item *Reference) error { 254 | if C.PyList_SetItem(self.Object, C.int64_t(index), item.Object) == 0 { 255 | return nil 256 | } else { 257 | return GetError() 258 | } 259 | } 260 | 261 | // 262 | // Dict 263 | // 264 | 265 | var DictType = NewType(&C.PyDict_Type) 266 | 267 | func NewDict() (*Reference, error) { 268 | if dict := C.PyDict_New(); dict != nil { 269 | return NewReference(dict), nil 270 | } else { 271 | return nil, GetError() 272 | } 273 | } 274 | 275 | func (self *Reference) IsDict() bool { 276 | // More efficient to use the flag 277 | return self.Type().HasFlag(C.Py_TPFLAGS_DICT_SUBCLASS) 278 | } 279 | 280 | func (self *Reference) SetDictItem(key *Reference, value *Reference) error { 281 | if C.PyDict_SetItem(self.Object, key.Object, value.Object) == 0 { 282 | return nil 283 | } else { 284 | return GetError() 285 | } 286 | } 287 | 288 | // 289 | // Set (mutable) 290 | // 291 | 292 | var SetType = NewType(&C.PySet_Type) 293 | 294 | func NewSet(iterable *Reference) (*Reference, error) { 295 | if set := C.PySet_New(iterable.Object); set != nil { 296 | return NewReference(set), nil 297 | } else { 298 | return nil, GetError() 299 | } 300 | } 301 | 302 | func (self *Reference) IsSet() bool { 303 | return self.Type().IsSubtype(SetType) 304 | } 305 | 306 | // 307 | // Frozen set (immutable) 308 | // 309 | 310 | var FrozenSetType = NewType(&C.PyFrozenSet_Type) 311 | 312 | func NewFrozenSet(iterable *Reference) (*Reference, error) { 313 | if frozenSet := C.PyFrozenSet_New(iterable.Object); frozenSet != nil { 314 | return NewReference(frozenSet), nil 315 | } else { 316 | return nil, GetError() 317 | } 318 | } 319 | 320 | func (self *Reference) IsFrozenSet() bool { 321 | return self.Type().IsSubtype(FrozenSetType) 322 | } 323 | 324 | // 325 | // Bytes (immutable) 326 | // 327 | 328 | var BytesType = NewType(&C.PyBytes_Type) 329 | 330 | func NewBytes(value []byte) (*Reference, error) { 331 | size := len(value) 332 | value_ := C.CBytes(value) 333 | defer C.free(value_) // TODO: check this! 334 | 335 | if bytes := C.PyBytes_FromStringAndSize((*C.char)(value_), C.int64_t(size)); bytes != nil { 336 | return NewReference(bytes), nil 337 | } else { 338 | return nil, GetError() 339 | } 340 | } 341 | 342 | func (self *Reference) IsBytes() bool { 343 | // More efficient to use the flag 344 | return self.Type().HasFlag(C.Py_TPFLAGS_BYTES_SUBCLASS) 345 | } 346 | 347 | func (self *Reference) ToBytes() ([]byte, error) { 348 | if pointer, size, err := self.AccessBytes(); err == nil { 349 | return C.GoBytes(pointer, C.int(size)), nil 350 | } else { 351 | return nil, err 352 | } 353 | } 354 | 355 | func (self *Reference) AccessBytes() (unsafe.Pointer, int, error) { 356 | if string_ := C.PyBytes_AsString(self.Object); string_ != nil { 357 | size := C.PyBytes_Size(self.Object) 358 | return unsafe.Pointer(string_), int(size), nil 359 | } else { 360 | return nil, 0, GetError() 361 | } 362 | } 363 | 364 | // 365 | // Byte array (mutable) 366 | // 367 | 368 | var ByteArrayType = NewType(&C.PyByteArray_Type) 369 | 370 | func NewByteArray(value []byte) (*Reference, error) { 371 | size := len(value) 372 | value_ := C.CBytes(value) 373 | defer C.free(value_) // TODO: check this! 374 | 375 | if byteArray := C.PyByteArray_FromStringAndSize((*C.char)(value_), C.int64_t(size)); byteArray != nil { 376 | return NewReference(byteArray), nil 377 | } else { 378 | return nil, GetError() 379 | } 380 | } 381 | 382 | func (self *Reference) IsByteArray() bool { 383 | return self.Type().IsSubtype(ByteArrayType) 384 | } 385 | 386 | func (self *Reference) ByteArrayToBytes() ([]byte, error) { 387 | if pointer, size, err := self.AccessByteArray(); err == nil { 388 | return C.GoBytes(pointer, C.int(size)), nil 389 | } else { 390 | return nil, err 391 | } 392 | } 393 | 394 | func (self *Reference) AccessByteArray() (unsafe.Pointer, int, error) { 395 | if string_ := C.PyByteArray_AsString(self.Object); string_ != nil { 396 | size := C.PyByteArray_Size(self.Object) 397 | return unsafe.Pointer(string_), int(size), nil 398 | } else { 399 | return nil, 0, GetError() 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /reference.go: -------------------------------------------------------------------------------- 1 | package python 2 | 3 | /* 4 | #define PY_SSIZE_T_CLEAN 5 | #include 6 | */ 7 | import "C" 8 | 9 | // 10 | // Reference 11 | // 12 | 13 | type Reference struct { 14 | Object *C.PyObject 15 | } 16 | 17 | func NewReference(pyObject *C.PyObject) *Reference { 18 | return &Reference{pyObject} 19 | } 20 | 21 | func (self *Reference) Type() *Type { 22 | return NewType(self.Object.ob_type) 23 | } 24 | -------------------------------------------------------------------------------- /scripts/_env: -------------------------------------------------------------------------------- 1 | 2 | _HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 3 | 4 | . "$_HERE/_functions" 5 | 6 | MODULE=github.com/tliron/py4go 7 | 8 | ROOT=$(readlink --canonicalize "$_HERE/..") 9 | 10 | GOPATH=${GOPATH:-$HOME/go} 11 | 12 | export TMPDIR=/Depot/Temporary 13 | -------------------------------------------------------------------------------- /scripts/_functions: -------------------------------------------------------------------------------- 1 | 2 | RED='\033[0;31m' 3 | GREEN='\033[0;32m' 4 | BLUE='\033[0;34m' 5 | CYAN='\033[0;36m' 6 | RESET='\033[0m' 7 | 8 | # Colored messages (blue is the default) 9 | # Examples: 10 | # m "hello world" 11 | # m "hello world" "$GREEN" 12 | function m () { 13 | local COLOR=${2:-$BLUE} 14 | echo -e "$COLOR$1$RESET" 15 | } 16 | 17 | function git_version () { 18 | VERSION=$(git -C "$ROOT" describe --tags --always) 19 | REVISION=$(git -C "$ROOT" rev-parse HEAD) 20 | TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S %Z") 21 | GO_VERSION=$(go version | { read _ _ v _; echo ${v#go}; }) 22 | } 23 | -------------------------------------------------------------------------------- /scripts/_trap: -------------------------------------------------------------------------------- 1 | 2 | function goodbye () { 3 | local DURATION=$(date --date=@$(( "$(date +%s)" - "$TRAP_START_TIME" )) --utc +%T) 4 | local CODE=$1 5 | cd "$TRAP_DIR" 6 | if [ "$CODE" == 0 ]; then 7 | m "$(realpath --relative-to="$ROOT" "$0") succeeded! $DURATION" "$GREEN" 8 | elif [ "$CODE" == abort ]; then 9 | m "Aborted $(realpath --relative-to="$ROOT" "$0")! $DURATION" "$RED" 10 | else 11 | m "Oh no! $(realpath --relative-to="$ROOT" "$0") failed! $DURATION" "$RED" 12 | fi 13 | } 14 | 15 | function trap_EXIT () { 16 | local ERR=$? 17 | goodbye "$ERR" 18 | exit "$ERR" 19 | } 20 | 21 | function trap_INT () { 22 | goodbye abort 23 | trap - EXIT 24 | exit 1 25 | } 26 | 27 | TRAP_DIR=$PWD 28 | TRAP_START_TIME=$(date +%s) 29 | 30 | trap trap_INT INT 31 | 32 | trap trap_EXIT EXIT 33 | -------------------------------------------------------------------------------- /scripts/example: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/_env" 6 | . "$HERE/_trap" 7 | 8 | function run () { 9 | local NAME=$1 10 | pushd "$ROOT/$NAME" > /dev/null 11 | go run . 12 | popd > /dev/null 13 | } 14 | 15 | run examples/hello-world 16 | -------------------------------------------------------------------------------- /scripts/format: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/_env" 6 | 7 | gofmt -w -s -e \ 8 | "$ROOT" \ 9 | "$ROOT/examples/hello-world/" \ 10 | "$ROOT/examples/hello-world/api" 11 | -------------------------------------------------------------------------------- /scripts/refresh-mod: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/_env" 6 | 7 | rm --force "$ROOT/go.mod" "$ROOT/go.sum" 8 | 9 | cd "$ROOT" 10 | go mod init "$MODULE" 11 | go mod tidy 12 | 13 | "$HERE/build" 14 | 15 | go mod tidy 16 | -------------------------------------------------------------------------------- /type.go: -------------------------------------------------------------------------------- 1 | package python 2 | 3 | /* 4 | #define PY_SSIZE_T_CLEAN 5 | #include 6 | */ 7 | import "C" 8 | 9 | // 10 | // Type 11 | // 12 | 13 | type Type struct { 14 | Object *C.PyTypeObject 15 | } 16 | 17 | func NewType(pyTypeObject *C.PyTypeObject) *Type { 18 | return &Type{pyTypeObject} 19 | } 20 | 21 | func (self *Type) IsSubtype(type_ *Type) bool { 22 | return C.PyType_IsSubtype(self.Object, type_.Object) != 0 23 | } 24 | 25 | func (self *Type) HasFlag(flag C.ulong) bool { 26 | return C.PyType_GetFlags(self.Object)&flag != 0 27 | } 28 | --------------------------------------------------------------------------------