Tal Liron 5 anni fa
commit
c56266134f
29 ha cambiato i file con 1507 aggiunte e 0 eliminazioni
  1. 4 0
      .gitignore
  2. 201 0
      LICENSE
  3. 6 0
      NOTICE
  4. 89 0
      README.md
  5. 11 0
      common.go
  6. 42 0
      concurrency.go
  7. 16 0
      examples/hello-world/api/functions.go
  8. 21 0
      examples/hello-world/api/go_.go
  9. 45 0
      examples/hello-world/api/module.go
  10. 52 0
      examples/hello-world/api/py_.go
  11. 27 0
      examples/hello-world/foo.py
  12. 67 0
      examples/hello-world/main.go
  13. 82 0
      exception.go
  14. 26 0
      general.go
  15. 3 0
      go.mod
  16. 2 0
      go.sum
  17. 25 0
      import.go
  18. 116 0
      module.go
  19. 90 0
      object.go
  20. 27 0
      path.go
  21. 399 0
      primitive.go
  22. 23 0
      reference.go
  23. 12 0
      scripts/_env
  24. 22 0
      scripts/_functions
  25. 32 0
      scripts/_trap
  26. 15 0
      scripts/example
  27. 11 0
      scripts/format
  28. 14 0
      scripts/refresh-mod
  29. 27 0
      type.go

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+.project
+__pycache__
+
+dist/

+ 201 - 0
LICENSE

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright {yyyy} {name of copyright owner}
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 6 - 0
NOTICE

@@ -0,0 +1,6 @@
+Custard.py
+Copyright 2020 Tal Liron
+
+---
+
+"Kubernetes" and "K8s" are registered trademarks of The Linux Foundation.

+ 89 - 0
README.md

@@ -0,0 +1,89 @@
+*Work in progress*
+
+py4go
+=====
+
+[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
+[![Go Report Card](https://goreportcard.com/badge/github.com/tliron/py4go)](https://goreportcard.com/report/github.com/tliron/py4go)
+
+Execute Python 3 code from within your Go program.
+
+With py4go you can also expose Go functions to be called from that Python code.
+
+
+Example
+-------
+
+```go
+package main
+
+import (
+    "fmt"
+    "github.com/tliron/py4go"
+)
+
+func main() {
+    // Initialize Python
+    python.Initialize()
+    defer python.Finalize()
+
+    // Import Python code (foo.py)
+    foo, _ := python.Import("foo")
+    defer foo.Release()
+
+    // Get access to a Python function
+    hello, _ := foo.GetAttr("hello")
+    defer hello.Release()
+
+    // Call the function with arguments
+    r, _ := hello.Call("myargument")
+    defer r.Release()
+    fmt.Printf("Returned: %s\n", r.String())
+}
+```
+
+See the [examples](examples/) directory for more detail.
+
+Generally speaking, calling Python code is easy but calling Go code from Python requires a lot more
+work, including writing embedded wrapper functions in C. Perhaps in the future we will be able to
+automate this.
+
+Caveats
+-------
+
+This is *not* an implementation of Python in Go. Rather, py4go works by embedding CPython into your
+Go program using [cgo](https://github.com/golang/go/wiki/cgo) functionality. The advantage of this
+approach is that you are using the standard Python runtime and can thus make use of the entire
+ecosystem of Python libraries, including wrappers for C libraries. But there are several issues to
+be aware of:
+
+* It's more difficult to distribute your Go program because you *must* have the CPython library
+  available on the target operating system with a specific name. Because different operating systems
+  have their own conventions for naming this library, to create a truly portable distribution it may
+  be best to distribute your program as a packaged container, e.g. using Flatpak or Docker.
+* It is similarly more difficult to *build* your Go program. We are using `pkg-config: python3-embed` to
+  locate the CPython SDK, which works on Fedora-based operating systems. But, because where you
+  *build* will determine the requirements for where you will *run*, it may be best to build on
+  Fedora, either directly or in a virtual machine or container. Unfortunately cgo does not let us
+  parameterize that `pkg-config` directive, thus you will have to modify our source files in order to
+  build on/for other operating systems.
+* Calling functions and passing data between these two high-level language's runtime environments
+  obviously incurs some overhead. Notably strings are sometimes copied multiple times internally,
+  and may be encoded and decoded (Go normally uses UTF-8, Python defaults to UCS4). If you are
+  frequently calling back and forth be aware of possible performance degradation. As always, if you
+  experience a problem measure first and identify the bottleneck before prematurely optimizing!
+* Similarly, be aware that you are simultaneously running two memory management runtimes, each with
+  its own heap allocation and garbage collection threads, and that Go is unaware of Python's. Your
+  Go code will thus need to explicitly call `Release` on all Python references to ensure that they are
+  garbage collected. Luckily, the `defer` keyword makes this easy enough in many circumstances.
+* Concurrency is a bit tricky in Python due to its infamous Global Interpreter Lock (GIL). If
+  you are calling Python code from a Goroutine make sure to call `python.SaveThreadState` and/or
+  `python.EnsureGilState` as appropriate. See the examples for more detail.
+
+
+References
+----------
+
+* [go-python](https://github.com/sbinet/go-python) is a similar and more mature project for Python
+  2.
+* [goPy](https://github.com/qur/gopy) is a much older project for Python 2.

+ 11 - 0
common.go

@@ -0,0 +1,11 @@
+package python
+
+// See:
+//  https://docs.python.org/3/c-api/
+//  https://github.com/golang/go/wiki/cgo
+//  https://www.datadoghq.com/blog/engineering/cgo-and-python/
+//  https://github.com/sbinet/go-python
+
+// #cgo pkg-config: python3-embed
+// #cgo LDFLAGS: -lpython3
+import "C"

+ 42 - 0
concurrency.go

@@ -0,0 +1,42 @@
+package python
+
+// See:
+//   https://docs.python.org/3/c-api/init.html
+
+/*
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+*/
+import "C"
+
+//
+// ThreadState
+//
+
+type ThreadState struct {
+	State *C.PyThreadState
+}
+
+func SaveThreadState() *ThreadState {
+	return &ThreadState{C.PyEval_SaveThread()}
+}
+
+func (self *ThreadState) Restore() {
+	C.PyEval_RestoreThread(self.State)
+}
+
+//
+// GilState
+//
+
+type GilState struct {
+	State C.PyGILState_STATE
+}
+
+func EnsureGilState() *GilState {
+	return &GilState{C.PyGILState_Ensure()}
+}
+
+func (self *GilState) Release() {
+	C.PyGILState_Release(self.State)
+}

+ 16 - 0
examples/hello-world/api/functions.go

@@ -0,0 +1,16 @@
+package api
+
+// Here we define our functions in plain Go
+
+import (
+	"fmt"
+)
+
+func sayGoodbye() {
+	fmt.Println("Go >> goodbye from Go!")
+}
+
+func concat(a string, b string) string {
+	fmt.Printf("Go >> concatenating %q and %q\n", a, b)
+	return a + " " + b
+}

+ 21 - 0
examples/hello-world/api/go_.go

@@ -0,0 +1,21 @@
+package api
+
+// Here we export cgo wrappers for our plain Go functions
+// They handle the conversion between Go and C types
+
+// Note: cgo exports cannot be in the same file as cgo preamble funtions,
+// which is why this file cannot be combined with "py_.go"
+
+import "C"
+
+//export go_api_sayGoodbye
+func go_api_sayGoodbye() {
+	// We could just export sayGoodbye directly because it has no arguments
+	// But for completion we are adding an export
+	sayGoodbye()
+}
+
+//export go_api_concat
+func go_api_concat(a *C.char, b *C.char) *C.char {
+	return C.CString(concat(C.GoString(a), C.GoString(b)))
+}

+ 45 - 0
examples/hello-world/api/module.go

@@ -0,0 +1,45 @@
+package api
+
+// Here we add our "py_" functions to the module
+
+// Note that this file could potentially be combined with "py_.go", but we preferred separation
+// For that reason we must forward-declare the "py_" functions in the cgo preamble
+
+import (
+	"github.com/tliron/py4go"
+)
+
+/*
+#cgo pkg-config: python3-embed
+
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+
+PyObject *py_api_sayGoodbye(PyObject *self, PyObject *unused);
+PyObject *py_api_concat(PyObject *self, PyObject *args);
+PyObject *py_api_concat_fast(PyObject *self, PyObject **args, Py_ssize_t nargs);
+*/
+import "C"
+
+func CreateModule() (*python.Reference, error) {
+	if module, err := python.CreateModule("api"); err == nil {
+		if err := module.AddModuleCFunctionNoArgs("say_goodbye", C.py_api_sayGoodbye); err != nil {
+			module.Release()
+			return nil, err
+		}
+
+		if err := module.AddModuleCFunctionArgs("concat", C.py_api_concat); err != nil {
+			module.Release()
+			return nil, err
+		}
+
+		if err := module.AddModuleCFunctionFastArgs("concat_fast", C.py_api_concat_fast); err != nil {
+			module.Release()
+			return nil, err
+		}
+
+		return module, nil
+	} else {
+		return nil, err
+	}
+}

+ 52 - 0
examples/hello-world/api/py_.go

@@ -0,0 +1,52 @@
+package api
+
+// Here we define Python wrappers in C for our "go_" functions
+// They handle the conversion between Python and C types
+
+// We are demonstrating the two supported Python ABIs:
+// 1) the more common PyCFunction ABI, which passes arguments as Python tuples and dicts, and
+// 2) the newer _PyCFunctionFast ABI, which passes arguments as a more efficient stack
+
+// Unfortunately we must use C and not Go because:
+// 1) the "PyArg_Parse_" functions all use variadic arguments, which are not supported by cgo, and
+// 2) the "PyArg_Parse_" functions unpack arguments to pointers, which we cannot implement in Go
+
+// Note: cgo exports cannot be in the same file as cgo preamble functions,
+// which is why this file cannot be combined with "go_.go"
+// and is also why must forward-declare the "go_" functions in the cgo preamble
+
+// See:
+//   https://docs.python.org/3/c-api/arg.html
+
+/*
+#cgo pkg-config: python3-embed
+
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+
+void go_api_sayGoodbye();
+char *go_api_concat(char*, char*);
+
+// PyCFunction signature
+PyObject *py_api_sayGoodbye(PyObject *self, PyObject *unused) {
+	go_api_sayGoodbye();
+	return Py_None;
+}
+
+// PyCFunction signature
+PyObject *py_api_concat(PyObject *self, PyObject *args) {
+	char *arg1 = NULL, *arg2 = NULL;
+	PyArg_ParseTuple(args, "ss", &arg1, &arg2);
+	char *r = go_api_concat(arg1, arg2);
+	return PyUnicode_FromString(r);
+}
+
+// _PyCFunctionFast signature
+PyObject *py_api_concat_fast(PyObject *self, PyObject **args, Py_ssize_t nargs) {
+	char *arg1 = NULL, *arg2 = NULL;
+	_PyArg_ParseStack(args, nargs, "ss", &arg1, &arg2);
+	char *r = go_api_concat(arg1, arg2);
+	return PyUnicode_FromString(r);
+}
+*/
+import "C"

+ 27 - 0
examples/hello-world/foo.py

@@ -0,0 +1,27 @@
+import api
+
+def hello(name):
+    print("Python >> Hello, " + name)
+    return "You are " + name
+
+def goodbye():
+    print("Python >> Calling Go functions")
+    api.say_goodbye()
+
+def bad():
+    raise Exception("this is a Python exception")
+
+def say_name():
+    print("Python >> The name is " + api.concat("Tal", "Liron"))
+
+def say_name_fast():
+    print("Python >> The name is " + api.concat_fast("Tal", "Liron"))
+
+class Person:
+    def __init__(self, name):
+        self.name = name
+
+    def greet(self):
+        print("Python >> Greetings, " + self.name)
+
+person = Person("Linus")

+ 67 - 0
examples/hello-world/main.go

@@ -0,0 +1,67 @@
+package main
+
+import (
+	"fmt"
+
+	"github.com/tliron/py4go"
+	"github.com/tliron/py4go/examples/hello-world/api"
+)
+
+func main() {
+	python.PrependPythonPath(".")
+
+	python.Initialize()
+	defer python.Finalize()
+
+	fmt.Printf("Go >> Python version:\n%s\n", python.Version())
+	fmt.Println()
+
+	fmt.Println("Go >> Type checking:")
+	float, _ := python.NewPrimitiveReference(1.0)
+	fmt.Printf("Go >> IsFloat: %t\n", float.IsFloat())
+	fmt.Println()
+
+	api, _ := api.CreateModule()
+	api.EnableModule()
+	defer api.Release()
+
+	foo, _ := python.Import("foo")
+	defer foo.Release()
+
+	fmt.Println("Go >> Calling a Python function:")
+	hello, _ := foo.GetAttr("hello")
+	defer hello.Release()
+	r, _ := hello.Call("Tal")
+	defer r.Release()
+	r_, _ := r.ToString()
+	fmt.Printf("Go >> Python function returned: %s\n", r_)
+	fmt.Println()
+
+	fmt.Println("Go >> Calling a Python method:")
+	person, _ := foo.GetAttr("person")
+	defer person.Release()
+	greet, _ := person.GetAttr("greet")
+	defer greet.Release()
+	greet.Call()
+	fmt.Println()
+
+	fmt.Println("Go >> Python exception as Go error:")
+	bad, _ := foo.GetAttr("bad")
+	defer bad.Release()
+	if _, err := bad.Call(); err != nil {
+		fmt.Printf("Go >> Error message: %s\n", err)
+	}
+	fmt.Println()
+
+	goodbye, _ := foo.GetAttr("goodbye")
+	defer goodbye.Release()
+	goodbye.Call()
+
+	sayName, _ := foo.GetAttr("say_name")
+	defer sayName.Release()
+	sayName.Call()
+
+	sayNameFast, _ := foo.GetAttr("say_name_fast")
+	defer sayNameFast.Release()
+	sayNameFast.Call()
+}

+ 82 - 0
exception.go

@@ -0,0 +1,82 @@
+package python
+
+// See:
+//   https://docs.python.org/3/c-api/exceptions.html
+
+import (
+	"errors"
+)
+
+/*
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+*/
+import "C"
+
+func HasException() bool {
+	return C.PyErr_Occurred() != nil
+}
+
+func GetError() error {
+	if exception := FetchException(); exception != nil {
+		return exception
+	} else {
+		return errors.New("Python error without an exception")
+	}
+}
+
+//
+// Exception
+//
+
+type Exception struct {
+	Type      *Reference
+	Value     *Reference
+	Traceback *Reference
+}
+
+func FetchException() *Exception {
+	var type_, value, traceback *C.PyObject
+	C.PyErr_Fetch(&type_, &value, &traceback)
+	if type_ != nil {
+		defer C.PyErr_Restore(type_, value, traceback)
+
+		var type__, value_, traceback_ *Reference
+
+		if type_ != nil {
+			type__ = NewReference(type_)
+		}
+
+		if value != nil {
+			value_ = NewReference(value)
+		}
+
+		if traceback != nil {
+			traceback_ = NewReference(traceback)
+		}
+
+		return NewExceptionRaw(type__, value_, traceback_)
+	} else {
+		return nil
+	}
+}
+
+func NewExceptionRaw(type_ *Reference, value *Reference, traceback *Reference) *Exception {
+	return &Exception{
+		Type:      type_,
+		Value:     value,
+		Traceback: traceback,
+	}
+}
+
+// error signature
+func (self *Exception) Error() string {
+	// TODO: include traceback?
+	if self.Value != nil {
+		return self.Value.String()
+	} else if self.Type != nil {
+		return self.Type.String()
+	} else {
+		return "malformed Python exception"
+	}
+}

+ 26 - 0
general.go

@@ -0,0 +1,26 @@
+package python
+
+// See:
+//   https://docs.python.org/3/c-api/init.html
+
+/*
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+*/
+import "C"
+
+func Initialize() {
+	C.Py_Initialize()
+}
+
+func Finalize() error {
+	if C.Py_FinalizeEx() == 0 {
+		return nil
+	} else {
+		return GetError()
+	}
+}
+
+func Version() string {
+	return C.GoString(C.Py_GetVersion())
+}

+ 3 - 0
go.mod

@@ -0,0 +1,3 @@
+module github.com/tliron/py4go
+
+go 1.15

+ 2 - 0
go.sum

@@ -0,0 +1,2 @@
+github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0=
+github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc=

+ 25 - 0
import.go

@@ -0,0 +1,25 @@
+package python
+
+// See:
+//   https://docs.python.org/3/c-api/import.html
+
+import (
+	"unsafe"
+)
+
+/*
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+*/
+import "C"
+
+func Import(name string) (*Reference, error) {
+	name_ := C.CString(name)
+	defer C.free(unsafe.Pointer(name_))
+
+	if import_ := C.PyImport_ImportModule(name_); import_ != nil {
+		return NewReference(import_), nil
+	} else {
+		return nil, GetError()
+	}
+}

+ 116 - 0
module.go

@@ -0,0 +1,116 @@
+package python
+
+import (
+	"unsafe"
+)
+
+/*
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+*/
+import "C"
+
+var ModuleType = NewType(&C.PyModule_Type)
+
+func CreateModule(name string) (*Reference, error) {
+	name_ := C.CString(name)
+	defer C.free(unsafe.Pointer(name_))
+
+	var definition C.PyModuleDef
+	definition.m_name = name_
+
+	if module := C.PyModule_Create2(&definition, C.PYTHON_ABI_VERSION); module != nil {
+		return NewReference(module), nil
+	} else {
+		return nil, GetError()
+	}
+}
+
+func NewModuleRaw(name string) (*Reference, error) {
+	name_ := C.CString(name)
+	defer C.free(unsafe.Pointer(name_))
+
+	if module := C.PyModule_New(name_); module != nil {
+		return NewReference(module), nil
+	} else {
+		return nil, GetError()
+	}
+}
+
+func (self *Reference) GetModuleName() (string, error) {
+	if name := C.PyModule_GetName(self.Object); name != nil {
+		defer C.free(unsafe.Pointer(name)) // TODO: need this?
+
+		return C.GoString(name), nil
+	} else {
+		return "", GetError()
+	}
+}
+
+func (self *Reference) EnableModule() error {
+	if name := C.PyModule_GetNameObject(self.Object); name != nil {
+		name_ := NewReference(name)
+		defer name_.Release()
+
+		if moduleDict := C.PyImport_GetModuleDict(); moduleDict != nil {
+			moduleDict_ := NewReference(moduleDict)
+			defer moduleDict_.Release()
+
+			return moduleDict_.SetDictItem(name_, self)
+		} else {
+			return GetError()
+		}
+	} else {
+		return GetError()
+	}
+}
+
+// PyCFunction signature, second argument unused
+func (self *Reference) AddModuleCFunctionNoArgs(name string, function unsafe.Pointer) error {
+	return self.addModuleCFunction(name, function, C.METH_NOARGS)
+}
+
+// PyCFunction signature, second argument is the Python argument
+func (self *Reference) AddModuleCFunctionOneArg(name string, function unsafe.Pointer) error {
+	return self.addModuleCFunction(name, function, C.METH_O)
+}
+
+// PyCFunction signature, second argument is tuple of Python arguments
+func (self *Reference) AddModuleCFunctionArgs(name string, function unsafe.Pointer) error {
+	return self.addModuleCFunction(name, function, C.METH_VARARGS)
+}
+
+// PyCFunctionWithKeywords signature
+func (self *Reference) AddModuleCFunctionArgsAndKeywords(name string, function unsafe.Pointer) error {
+	return self.addModuleCFunction(name, function, C.METH_VARARGS|C.METH_KEYWORDS)
+}
+
+// _PyCFunctionFast signature
+func (self *Reference) AddModuleCFunctionFastArgs(name string, function unsafe.Pointer) error {
+	return self.addModuleCFunction(name, function, C.METH_FASTCALL)
+}
+
+// _PyCFunctionFastWithKeywords signature
+func (self *Reference) AddModuleCFunctionFastArgsAndKeywords(name string, function unsafe.Pointer) error {
+	return self.addModuleCFunction(name, function, C.METH_FASTCALL|C.METH_KEYWORDS)
+}
+
+func (self *Reference) addModuleCFunction(name string, function unsafe.Pointer, flags C.int) error {
+	name_ := C.CString(name)
+	defer C.free(unsafe.Pointer(name_))
+
+	methodDef := []C.PyMethodDef{
+		{
+			ml_name:  name_,
+			ml_meth:  C.PyCFunction(function),
+			ml_flags: flags,
+		},
+		{}, // NULL end of array
+	}
+
+	if C.PyModule_AddFunctions(self.Object, &methodDef[0]) == 0 {
+		return nil
+	} else {
+		return GetError()
+	}
+}

+ 90 - 0
object.go

@@ -0,0 +1,90 @@
+package python
+
+// See:
+//   https://docs.python.org/3/c-api/object.html
+
+import (
+	"unsafe"
+)
+
+/*
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+*/
+import "C"
+
+// fmt.Stringer interface
+func (self *Reference) String() string {
+	if str := C.PyObject_Str(self.Object); str != nil {
+		if data := C.PyUnicode_AsUTF8String(str); data != nil {
+			defer C.Py_DecRef(data)
+			if string_ := C.PyBytes_AsString(data); string_ != nil {
+				return C.GoString(string_)
+			} else {
+				return ""
+			}
+		} else {
+			return ""
+		}
+	} else {
+		return ""
+	}
+}
+
+func (self *Reference) Acquire() {
+	C.Py_IncRef(self.Object)
+}
+
+func (self *Reference) Release() {
+	C.Py_DecRef(self.Object)
+}
+
+func (self *Reference) Str() (*Reference, error) {
+	if str := C.PyObject_Str(self.Object); str != nil {
+		return NewReference(str), nil
+	} else {
+		return nil, GetError()
+	}
+}
+
+func (self *Reference) GetAttr(name string) (*Reference, error) {
+	name_ := C.CString(name)
+	defer C.free(unsafe.Pointer(name_))
+
+	if attr := C.PyObject_GetAttrString(self.Object, name_); attr != nil {
+		return NewReference(attr), nil
+	} else {
+		return nil, GetError()
+	}
+}
+
+func (self *Reference) SetAttr(name string, reference *Reference) error {
+	name_ := C.CString(name)
+	defer C.free(unsafe.Pointer(name_))
+
+	if C.PyObject_SetAttrString(self.Object, name_, reference.Object) == 0 {
+		return nil
+	} else {
+		return GetError()
+	}
+}
+
+func (self *Reference) Call(args ...interface{}) (*Reference, error) {
+	if args_, err := NewTuple(args...); err == nil {
+		if kw, err := NewDict(); err == nil {
+			return self.CallRaw(args_, kw)
+		} else {
+			return nil, err
+		}
+	} else {
+		return nil, err
+	}
+}
+
+func (self *Reference) CallRaw(args *Reference, kw *Reference) (*Reference, error) {
+	if r := C.PyObject_Call(self.Object, args.Object, kw.Object); r != nil {
+		return NewReference(r), nil
+	} else {
+		return nil, GetError()
+	}
+}

+ 27 - 0
path.go

@@ -0,0 +1,27 @@
+package python
+
+import (
+	"os"
+	"path/filepath"
+)
+
+const PYTHONPATH = "PYTHONPATH"
+
+func SetPythonPath(path ...string) {
+	path_ := filepath.Join(path...)
+	os.Setenv(PYTHONPATH, path_)
+}
+
+func AppendPythonPath(path ...string) {
+	path_ := filepath.SplitList(os.Getenv(PYTHONPATH))
+	path_ = append(path_, path...)
+	path__ := filepath.Join(path_...)
+	os.Setenv(PYTHONPATH, path__)
+}
+
+func PrependPythonPath(path ...string) {
+	path_ := filepath.SplitList(os.Getenv(PYTHONPATH))
+	path_ = append(path, path_...)
+	path__ := filepath.Join(path_...)
+	os.Setenv(PYTHONPATH, path__)
+}

+ 399 - 0
primitive.go

@@ -0,0 +1,399 @@
+package python
+
+// See:
+//   https://docs.python.org/3/c-api/concrete.html
+
+import (
+	"fmt"
+	"unsafe"
+)
+
+/*
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+*/
+import "C"
+
+func NewPrimitiveReference(value interface{}) (*Reference, error) {
+	if value == nil {
+		return None, nil
+	}
+
+	switch value_ := value.(type) {
+	case *Reference:
+		return value_, nil
+	case bool:
+		if value_ {
+			return True, nil
+		} else {
+			return False, nil
+		}
+	case int64:
+		return NewLong(value_)
+	case int32:
+		return NewLong(int64(value_))
+	case int8:
+		return NewLong(int64(value_))
+	case int:
+		return NewLong(int64(value_))
+	case float64:
+		return NewFloat(value_)
+	case float32:
+		return NewFloat(float64(value_))
+	case string:
+		return NewUnicode(value_)
+	case []interface{}:
+		return NewList(value_...)
+	case []byte:
+		return NewBytes(value_)
+	}
+
+	return nil, fmt.Errorf("unsupported primitive: %s", value)
+}
+
+//
+// None
+//
+
+var None = NewReference(C.Py_None)
+
+//
+// Bool
+//
+
+var BoolType = NewType(&C.PyBool_Type)
+
+var True = NewReference(C.Py_True)
+var False = NewReference(C.Py_False)
+
+func (self *Reference) IsBool() bool {
+	return self.Type().IsSubtype(BoolType)
+}
+
+func (self *Reference) ToBool() bool {
+	switch self.Object {
+	case C.Py_True:
+		return true
+	case C.Py_False:
+		return false
+	}
+	return false
+}
+
+//
+// Long
+//
+
+var LongType = NewType(&C.PyLong_Type)
+
+func NewLong(value int64) (*Reference, error) {
+	if long := C.PyLong_FromLong(C.long(value)); long != nil {
+		return NewReference(long), nil
+	} else {
+		return nil, GetError()
+	}
+}
+
+func (self *Reference) IsLong() bool {
+	// More efficient to use the flag
+	return self.Type().HasFlag(C.Py_TPFLAGS_LONG_SUBCLASS)
+}
+
+func (self *Reference) ToInt64() (int64, error) {
+	if long := C.PyLong_AsLong(self.Object); !HasException() {
+		return int64(long), nil
+	} else {
+		return 0, GetError()
+	}
+}
+
+//
+// Float
+//
+
+var FloatType = NewType(&C.PyFloat_Type)
+
+func NewFloat(value float64) (*Reference, error) {
+	if float := C.PyFloat_FromDouble(C.double(value)); float != nil {
+		return NewReference(float), nil
+	} else {
+		return nil, GetError()
+	}
+}
+
+func (self *Reference) IsFloat() bool {
+	return self.Type().IsSubtype(FloatType)
+}
+
+func (self *Reference) ToFloat64() (float64, error) {
+	if double := C.PyFloat_AsDouble(self.Object); !HasException() {
+		return float64(double), nil
+	} else {
+		return 0.0, GetError()
+	}
+}
+
+//
+// Unicode
+//
+
+var UnicodeType = NewType(&C.PyUnicode_Type)
+
+func NewUnicode(value string) (*Reference, error) {
+	value_ := C.CString(value)
+	defer C.free(unsafe.Pointer(value_))
+
+	if unicode := C.PyUnicode_FromString(value_); unicode != nil {
+		return NewReference(unicode), nil
+	} else {
+		return nil, GetError()
+	}
+}
+
+func (self *Reference) IsUnicode() bool {
+	// More efficient to use the flag
+	return self.Type().HasFlag(C.Py_TPFLAGS_UNICODE_SUBCLASS)
+}
+
+func (self *Reference) ToString() (string, error) {
+	if utf8string := C.PyUnicode_AsUTF8String(self.Object); utf8string != nil {
+		defer C.Py_DecRef(utf8string)
+
+		if string_ := C.PyBytes_AsString(utf8string); string_ != nil {
+			return C.GoString(string_), nil
+		} else {
+			return "", GetError()
+		}
+	} else {
+		return "", GetError()
+	}
+}
+
+//
+// Tuple
+//
+
+var TupleType = NewType(&C.PyTuple_Type)
+
+func NewTuple(items ...interface{}) (*Reference, error) {
+	if tuple, err := NewTupleRaw(len(items)); err == nil {
+		for index, item := range items {
+			if item_, err := NewPrimitiveReference(item); err == nil {
+				if err := tuple.SetTupleItem(index, item_); err != nil {
+					return nil, err
+				}
+			} else {
+				return nil, err
+			}
+		}
+		return tuple, nil
+	} else {
+		return nil, GetError()
+	}
+}
+
+func NewTupleRaw(size int) (*Reference, error) {
+	if tuple := C.PyTuple_New(C.long(size)); tuple != nil {
+		return NewReference(tuple), nil
+	} else {
+		return nil, GetError()
+	}
+}
+
+func (self *Reference) IsTuple() bool {
+	// More efficient to use the flag
+	return self.Type().HasFlag(C.Py_TPFLAGS_TUPLE_SUBCLASS)
+}
+
+func (self *Reference) SetTupleItem(index int, item *Reference) error {
+	if C.PyTuple_SetItem(self.Object, C.long(index), item.Object) == 0 {
+		return nil
+	} else {
+		return GetError()
+	}
+}
+
+//
+// List
+//
+
+var ListType = NewType(&C.PyList_Type)
+
+func NewList(items ...interface{}) (*Reference, error) {
+	if list, err := NewListRaw(len(items)); err == nil {
+		for index, item := range items {
+			if item_, err := NewPrimitiveReference(item); err == nil {
+				if err := list.SetListItem(index, item_); err != nil {
+					return nil, err
+				}
+			} else {
+				return nil, err
+			}
+		}
+		return list, nil
+	} else {
+		return nil, GetError()
+	}
+}
+
+func NewListRaw(size int) (*Reference, error) {
+	if list := C.PyList_New(C.long(size)); list != nil {
+		return NewReference(list), nil
+	} else {
+		return nil, GetError()
+	}
+}
+
+func (self *Reference) IsList() bool {
+	return self.Type().HasFlag(C.Py_TPFLAGS_LIST_SUBCLASS)
+}
+
+func (self *Reference) SetListItem(index int, item *Reference) error {
+	if C.PyList_SetItem(self.Object, C.long(index), item.Object) == 0 {
+		return nil
+	} else {
+		return GetError()
+	}
+}
+
+//
+// Dict
+//
+
+var DictType = NewType(&C.PyDict_Type)
+
+func NewDict() (*Reference, error) {
+	if dict := C.PyDict_New(); dict != nil {
+		return NewReference(dict), nil
+	} else {
+		return nil, GetError()
+	}
+}
+
+func (self *Reference) IsDict() bool {
+	// More efficient to use the flag
+	return self.Type().HasFlag(C.Py_TPFLAGS_DICT_SUBCLASS)
+}
+
+func (self *Reference) SetDictItem(key *Reference, value *Reference) error {
+	if C.PyDict_SetItem(self.Object, key.Object, value.Object) == 0 {
+		return nil
+	} else {
+		return GetError()
+	}
+}
+
+//
+// Set (mutable)
+//
+
+var SetType = NewType(&C.PySet_Type)
+
+func NewSet(iterable *Reference) (*Reference, error) {
+	if set := C.PySet_New(iterable.Object); set != nil {
+		return NewReference(set), nil
+	} else {
+		return nil, GetError()
+	}
+}
+
+func (self *Reference) IsSet() bool {
+	return self.Type().IsSubtype(SetType)
+}
+
+//
+// Frozen set (immutable)
+//
+
+var FrozenSetType = NewType(&C.PyFrozenSet_Type)
+
+func NewFrozenSet(iterable *Reference) (*Reference, error) {
+	if frozenSet := C.PyFrozenSet_New(iterable.Object); frozenSet != nil {
+		return NewReference(frozenSet), nil
+	} else {
+		return nil, GetError()
+	}
+}
+
+func (self *Reference) IsFrozenSet() bool {
+	return self.Type().IsSubtype(FrozenSetType)
+}
+
+//
+// Bytes (immutable)
+//
+
+var BytesType = NewType(&C.PyBytes_Type)
+
+func NewBytes(value []byte) (*Reference, error) {
+	size := len(value)
+	value_ := C.CBytes(value)
+	defer C.free(value_) // TODO: check this!
+
+	if bytes := C.PyBytes_FromStringAndSize((*C.char)(value_), C.long(size)); bytes != nil {
+		return NewReference(bytes), nil
+	} else {
+		return nil, GetError()
+	}
+}
+
+func (self *Reference) IsBytes() bool {
+	// More efficient to use the flag
+	return self.Type().HasFlag(C.Py_TPFLAGS_BYTES_SUBCLASS)
+}
+
+func (self *Reference) ToBytes() ([]byte, error) {
+	if pointer, size, err := self.AccessBytes(); err == nil {
+		return C.GoBytes(pointer, C.int(size)), nil
+	} else {
+		return nil, err
+	}
+}
+
+func (self *Reference) AccessBytes() (unsafe.Pointer, int, error) {
+	if string_ := C.PyBytes_AsString(self.Object); string_ != nil {
+		size := C.PyBytes_Size(self.Object)
+		return unsafe.Pointer(string_), int(size), nil
+	} else {
+		return nil, 0, GetError()
+	}
+}
+
+//
+// Byte array (mutable)
+//
+
+var ByteArrayType = NewType(&C.PyByteArray_Type)
+
+func NewByteArray(value []byte) (*Reference, error) {
+	size := len(value)
+	value_ := C.CBytes(value)
+	defer C.free(value_) // TODO: check this!
+
+	if byteArray := C.PyByteArray_FromStringAndSize((*C.char)(value_), C.long(size)); byteArray != nil {
+		return NewReference(byteArray), nil
+	} else {
+		return nil, GetError()
+	}
+}
+
+func (self *Reference) IsByteArray() bool {
+	return self.Type().IsSubtype(ByteArrayType)
+}
+
+func (self *Reference) ByteArrayToBytes() ([]byte, error) {
+	if pointer, size, err := self.AccessByteArray(); err == nil {
+		return C.GoBytes(pointer, C.int(size)), nil
+	} else {
+		return nil, err
+	}
+}
+
+func (self *Reference) AccessByteArray() (unsafe.Pointer, int, error) {
+	if string_ := C.PyByteArray_AsString(self.Object); string_ != nil {
+		size := C.PyByteArray_Size(self.Object)
+		return unsafe.Pointer(string_), int(size), nil
+	} else {
+		return nil, 0, GetError()
+	}
+}

+ 23 - 0
reference.go

@@ -0,0 +1,23 @@
+package python
+
+/*
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+*/
+import "C"
+
+//
+// Reference
+//
+
+type Reference struct {
+	Object *C.PyObject
+}
+
+func NewReference(pyObject *C.PyObject) *Reference {
+	return &Reference{pyObject}
+}
+
+func (self *Reference) Type() *Type {
+	return NewType(self.Object.ob_type)
+}

+ 12 - 0
scripts/_env

@@ -0,0 +1,12 @@
+
+_HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")")
+
+. "$_HERE/_functions"
+
+MODULE=github.com/tliron/py4go
+
+ROOT=$(readlink --canonicalize "$_HERE/..")
+
+GOPATH=${GOPATH:-$HOME/go}
+
+export TMPDIR=/Depot/Temporary

+ 22 - 0
scripts/_functions

@@ -0,0 +1,22 @@
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+BLUE='\033[0;34m'
+CYAN='\033[0;36m'
+RESET='\033[0m'
+
+# Colored messages (blue is the default)
+# Examples:
+#   m "hello world"
+#   m "hello world" "$GREEN"
+function m () {
+	local COLOR=${2:-$BLUE}
+	echo -e "$COLOR$1$RESET"
+}
+
+function git_version () {
+	VERSION=$(git -C "$ROOT" describe --tags --always)
+	REVISION=$(git -C "$ROOT" rev-parse HEAD)
+	TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S %Z")
+	GO_VERSION=$(go version | { read _ _ v _; echo ${v#go}; })
+}

+ 32 - 0
scripts/_trap

@@ -0,0 +1,32 @@
+
+function goodbye () {
+	local DURATION=$(date --date=@$(( "$(date +%s)" - "$TRAP_START_TIME" )) --utc +%T)
+	local CODE=$1
+	cd "$TRAP_DIR"
+	if [ "$CODE" == 0 ]; then
+		m "$(realpath --relative-to="$ROOT" "$0") succeeded! $DURATION" "$GREEN"
+	elif [ "$CODE" == abort ]; then
+		m "Aborted $(realpath --relative-to="$ROOT" "$0")! $DURATION" "$RED"
+	else
+		m "Oh no! $(realpath --relative-to="$ROOT" "$0") failed! $DURATION" "$RED"
+	fi
+}
+
+function trap_EXIT () {
+	local ERR=$?
+	goodbye "$ERR"
+	exit "$ERR"
+}
+
+function trap_INT () {
+	goodbye abort
+	trap - EXIT
+	exit 1
+}
+
+TRAP_DIR=$PWD
+TRAP_START_TIME=$(date +%s)
+
+trap trap_INT INT
+
+trap trap_EXIT EXIT

+ 15 - 0
scripts/example

@@ -0,0 +1,15 @@
+#!/bin/bash
+set -e
+
+HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")")
+. "$HERE/_env"
+. "$HERE/_trap"
+
+function run () {
+	local TOOL=$1
+	pushd "$ROOT/$TOOL" > /dev/null
+	go run .
+	popd > /dev/null
+}
+
+run examples/hello-world

+ 11 - 0
scripts/format

@@ -0,0 +1,11 @@
+#!/bin/bash
+set -e
+
+HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")")
+. "$HERE/_env"
+
+gofmt -w -s -e \
+	"$ROOT" \
+	"$ROOT/examples/hello-world/" \
+	"$ROOT/examples/hello-world/api"
+	

+ 14 - 0
scripts/refresh-mod

@@ -0,0 +1,14 @@
+#!/bin/bash
+set -e
+
+HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")")
+. "$HERE/_env"
+
+rm --force "$ROOT/go.mod" "$ROOT/go.sum"
+
+cd "$ROOT"
+go mod init "$MODULE"
+
+"$HERE/build"
+
+go mod tidy

+ 27 - 0
type.go

@@ -0,0 +1,27 @@
+package python
+
+/*
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+*/
+import "C"
+
+//
+// Type
+//
+
+type Type struct {
+	Object *C.PyTypeObject
+}
+
+func NewType(pyTypeObject *C.PyTypeObject) *Type {
+	return &Type{pyTypeObject}
+}
+
+func (self *Type) IsSubtype(type_ *Type) bool {
+	return C.PyType_IsSubtype(self.Object, type_.Object) != 0
+}
+
+func (self *Type) HasFlag(flag C.ulong) bool {
+	return C.PyType_GetFlags(self.Object)&flag != 0
+}