class: center, middle # See Python, See Python Go, Go Python Go Presented by **Andrey Petrov** at **PyCon 2016** --- class: center, middle # Andrey Petrov ## @shazow ![shazow](images/shazow.png) *[urllib3](https://github.com/shazow/urllib3), [ssh-chat](https://github.com/shazow/ssh-chat), [briefmetrics](https://briefmetrics.com/), [colorblendy](http://colorblendy.com/), [tweepsect](http://tweepsect.com/), [funny tweets](https://twitter.com/shazow), and [more](https://shazow.net/)* --- # Do you Go? -- - Simple syntax that is easy to learn -- - Compiles superfast -- - Statically typed -- - Statically linked binaries -- - Cross-compiles to ~every platform -- - Easy concurrency -- - Great standard library -- - Gophers! --- class: middle, center # Python ๐ Go .gopher[![PyGopher](images/pygopher.png)] .footnote[Credit: Renee French & Alice Tribuleva] --- # Go in Python .big[ ```python import os os.system("go run main.go") ``` ] -- .center[ ## *lol just kidding* ## ๐ ] --- # Fun Facts - Python speaks with C -- - Go speaks with C -- - Therefore, Python speaks with Go? ??? C can act as a bridge between any two languages that can call to/from C. --- # Challenges 1. Runtime barriers -- - Garbage Collectors (GC) - Global Interpreter Lock (GIL) - Just In Time compiling (JIT) - Resource Pools (Threads) -- 2. Syntax and feature barriers -- - Go: Interface, Goroutines, etc. - Python: Classes, Generators, etc. - Oodles of other language-specific constructs --- class: full-image ![Runtimes](images/runtimes.svg) ??? C land is a beautiful barren wasteland of buffer overflows, memory leaks, and segmentation faults. --- # Let's take a moment and imagine... --- # Running a webserver in Go ```go package main import ( "fmt" "net/http" ) func index(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "Hello, world.\n") } func main() { http.HandleFunc("/", index) http.ListenAndServe("127.0.0.1:5000", nil) } ``` --- # Running a webserver in Python ```python from flask import Flask app = Flask(__name__) @app.route('/') def index(): return 'Hello, world!\n' if __name__ == '__main__': app.run(host='127.0.0.1', port=5000) ``` --- # Running a Go webserver in Python?? ```python from gohttp import route, run @route('/') def index(w, req): w.write("Hello, world.\n") if __name__ == '__main__': run(host='127.0.0.1', port=5000) ``` -- ### Compare ```python from flask import Flask app = Flask(__name__) @app.route('/') def index(): return 'Hello, world!\n' if __name__ == '__main__': app.run(host='127.0.0.1', port=5000) ``` --- # Comparing handlers #### Go (net/http) ```go func index(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "Hello, world.\n") } ``` #### Go in Python (gohttplib) ```python def index(w, req): w.write("Hello, world.\n") ``` #### Python (flask) ```python def index(): return 'Hello, world!\n' ``` ??? Translating a Go API into Python is a fun subjective exercise. It doesn't need to look like this, but it's a good example. --- class: center # gohttplib ### This is an actual functioning thing. [github.com/shazow/gohttplib](https://github.com/shazow/gohttplib) -- ### ### Sponsored by [Glider Labs](http://gliderlabs.com/) ### ๐ --- class: center, middle # Here's how it works... --- # The Plan 1. Go: Export Go functions to a C shared library 2. C: ๐ฐ 3. Python: Call C and wrap it in a Python-shaped bow 4. Make it actually work ยฏ\\\_(ใ)\_/ยฏ --- class: full-image ![World of Go](images/worldofgo.png) --- # Calling C from Go ```go package main /* int the_answer() { return 42; } */ import "C" import "fmt" func main() { r := C.the_answer() fmt.Println(r) } ``` -- ```bash $ go build -o answer $ ./answer 42 ``` --- # Calling Go from C ```go package main import "C" //export TheAnswer func TheAnswer() C.int { return C.int(42) } func main() {} ``` -- ```bash $ go build -buildmode=c-shared -o libanswer.so ``` -- ```c #include
#include "libanswer.h" int main() { int r = TheAnswer(); printf("%d\n", r); return 0; } ``` --- class: full-image ![World of Python](images/worldofpython.png) --- # See CPython C - [CPython Extension Interface](https://docs.python.org/3/extending/extending.html): no dependencies, but lots of boilerplate - [CFFI](https://cffi.readthedocs.io/): a little more magic but does more work for us and more portable ??? If you've ever seen PyObject and whatnot littered around a C file, that's the CPython extension interface. On the other hand, CFFI is also available in PyPy and other fun implementations of the language. --- # Calling C from Python ```python # answer_build.py: from cffi import FFI ffi = FFI() ffi.cdef("int the_answer();") ffi.set_source("_answer", """ int the_answer() { return 42; } """) if __name__ == "__main__": ffi.compile() ``` -- ```bash $ python answer_build.py $ ls _answer.c _answer.o _answer.so answer_build.py ``` -- ```python # answer.py: from _answer import lib print(lib.the_answer()) ``` --- # Calling Python from C Simple function pointer that can be used in C: ```python @ffi.callback("int(int, int)") def add(x, y): return x + y ``` -- .center[ ### ๐ ~handwaving~ ### ๐ ] -- ```c static int (*add)(int x, int b); ``` That's all we need for now. [More on CFFI embedding here](http://cffi.readthedocs.io/en/latest/embedding.html). --- # Challenges 1. Runtime barriers - Garbage Collectors (GC) - Global Interpreter Lock (GIL) - Just In Time compiling in PyPy (JIT) - Thread Pools 2. Syntax and feature barriers - Go: Interface, Goroutines, etc. - Python: Classes, Generators, etc. - Oodles of other language-specific constructs --- # Overcoming Challenges 1. **Don't share memory** --- class: full-image ![Runtimes](images/runtimes.svg) --- # Challenge 1: Don't share memory ```go http.HandleFunc(pattern, func(w http.ResponseWriter, req *http.Request) { ... }) ``` Our Python code will need to provide this callback, but we can't pass `*http.Request` to Python. -- ```c typedef struct Request_ { const char *Method; const char *Host; ... } Request; ``` -- ```go http.HandleFunc(pattern, func(w http.ResponseWriter, req *http.Request) { // Wrap relevant request fields in a C-friendly datastructure. creq := C.Request{ Method: C.CString(req.Method), Host: C.CString(req.Host), ... } ... ``` --- # Overcoming Challenges 1. Don't share memory 2. **Translation layer** --- # Challenge 2: Translation layer ```go http.HandleFunc(pattern, func(w http.ResponseWriter, req *http.Request) { ... }) ``` Our Python code will need to use the `http.ResponseWriter` interface. -- ```go ResponseWriter.Write([]byte) (int, error) ResponseWriter.WriteHeader(int) ``` -- ```go //export ResponseWriter_Write func ResponseWriter_Write(wPtr C.uint, cbuf *C.char, length C.int) C.int { buf := C.GoBytes(unsafe.Pointer(cbuf), length) ... n, _ := (*(*http.ResponseWriter)(w)).Write(buf) return C.int(n) } //export ResponseWriter_WriteHeader func ResponseWriter_WriteHeader(wPtr C.uint, header C.int) { ... (*(*http.ResponseWriter)(w)).WriteHeader(int(header)) } ``` --- # Challenge 2: Translation layer (Cont.) We exported these functions in Go ```go ResponseWriter_Write(wPtr C.uint, cbuf *C.char, length C.int) C.int ResponseWriter_WriteHeader(wPtr C.uint, header C.int) ``` Now we can wrap them in Python ```python lib = ffi.dlopen(...) class ResponseWriter: def __init__(self, w): self._w = w def write(self, body): n = lib.ResponseWriter_Write(self._w, body, len(body)) if n != len(body): raise IOError("Failed to write to ResponseWriter.") def set_status(self, code): lib.ResponseWriter_WriteHeader(self._w, code) ``` --- # Challenge 2: Translation layer (Cont.) Original interface in Go: ```go type http.ResponseWriter interface { WriteHeader(int) } ``` -- Exported interface from Go: ```go func ResponseWriter_WriteHeader(wPtr C.uint, header C.int) ``` -- C header: ```c void ResponseWriter_WriteHeader(unsigned int p0, int p1); ``` -- Accessing C from Python: ```python ResponseWriter_WriteHeader(w, header) ``` -- Python wrapper: ```python ResponseWriter.set_status(self, code) ``` --- # Overcoming Challenges 1. Don't share memory 2. Translation layer 3. **Seriously, don't share memory** --- # Challenge 3: Seriously, don't share memory * Wait, what's a Go interface? -- * To use it, we need to pass a *pointer* through โ Go โข C โข Python โข C โข Go โ -- * Solution: Pointer Proxy! ๐ --- class: full-image # Pointer Proxy ![Pointer Proxy](images/ptrproxy.svg) --- # Pointer Proxy ```go type ptrProxy struct { sync.Mutex count uint lookup map[uint]unsafe.Pointer } func (p *ptrProxy) Ref(ptr unsafe.Pointer) C.uint { ... } func (p *ptrProxy) Deref(id C.uint) (unsafe.Pointer, bool) { ... } func (p *ptrProxy) Free(id C.uint) { ... } ``` Now we can pass an opaque uint across enemy lines and it's perfectly safe. --- # Quick flashback: Translation layer One pointer proxy to rule them all. ๐ ```go var cpointers = PtrProxy() ``` -- In the callback, reference to bind them. โ ```go http.HandleFunc(pattern, func(w http.ResponseWriter, req *http.Request) { // Wrap relevant request fields in a C-friendly datastructure. creq := C.Request{ ... } wPtr := cpointers.Ref(unsafe.Pointer(&w)) ... cpointers.Free(wPtr) }) ``` -- With pointer proxy, dereference to find them. ๐ ```go func ResponseWriter_WriteHeader(wPtr C.uint, header C.int) { w, _ := cpointers.Deref(wPtr) (*(*http.ResponseWriter)(w)).WriteHeader(int(header)) } ``` --- # Recap * If our languages can speak with C, they can speak with each other. ??? - C makes for a handy bridge between languages. Other languages without required runtimes can also work, like Rust. -- * Be careful going in and out of runtimes. ??? - Conflicting runtimes throw all assumptions out the window, memory can move, it can disappear, it can change. -- * Be super-careful with sharing memory. ??? - Copy data into C-friendly structs. - Pointer proxies are handy for passing a reference to some memory without the memory itself. -- * We'll need a translation layer to use non-trivial language constructs. ??? - C code is C code, but once the call reaches our destination we can make it look as pretty and idiomatic as we'd like. --- # Other Considerations -- * Memory leaks ๐ฆ ??? - The examples here are super rough, you'll likely need to explicitly free any memory allocated for C. -- * Race conditions ๐ ??? - If you're not careful with GILs and whatnot, you'll likely get race conditions. -- * Context switching overhead ๐ฉโ๐ฉโ๐งโ๐ฆ ??? - Switching across runtimes does have overhead, such as thread-switching. This is a fairly expensive constant-time operation in Go. -- * Probably security issues because C is hard ๐ฉ ??? - Anytime you're touching C, beware of vuln dragons. -- * Architecture campanelle ๐ ??? - Spaghetti can be delicious but messy. --- # A Palate Cleanser -- .center[ ## โจ๐๐ ## LOLBENCHMARKS! ## ๐๐ฅ๐ฎ ] --- class: full-image, center # LOLBENCHMARKS ![lolbenchmarks](images/lolbenchmarks.png) --- class: center # LOLBENCHMARKS | Name | Total | Req/Sec | Time/Req | |-|-|-|-| | go-net/http | 1.115 | 8969.89 | 0.111 | | gohttp-c | 1.181 | 8470.97 | 0.118 | | gohttp-python | 1.285 | 7779.87 | 0.129 | | gunicorn-flask | 7.826 | 1277.73 | 0.783 | | werkzeug-flask | 15.029 | 665.37 | 1.503 | # ๐
Conditions: `ab` doing 10,000 requests with 10 concurrency on my ๐ป.
--- # Thank you * [github.com/shazow/gohttplib](https://github.com/shazow/gohttplib) * [hrku.co/gopythongo](https://hrku.co/gopythongo) * [twitter.com/shazow](https://twitter.com/shazow) * [shazow.net](https://shazow.net/) .center[ ![shazow](images/shazow.png) ]