main.go (601 lines of code) (raw):
package main
/*
#define Py_LIMITED_API
#include <Python.h>
#include <stdlib.h>
#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
#include <numpy/arrayobject.h>
PyObject *get_go_vncdriver_VNCSession_type();
PyObject *GoPyArray_SimpleNew(int nd, npy_intp* dims, int typenum);
PyObject *GoPyArray_SimpleNewFromData(int nd, npy_intp* dims, int typenum, void *data);
void PyErr_SetGoVNCDriverError(char* msg);
// Workaround missing variadic function support
// https://github.com/golang/go/issues/975
static int PyArg_ParseTuple_list_O(PyObject *args, PyObject **a) {
return PyArg_ParseTuple(args, "O", &PyList_Type, a);
}
static int PyArg_ParseTuple_connect(PyObject *args, PyObject *kwds, char **name, char **address, char **password, char **encoding, int *quality_level, int *compress_level, int *fine_quality_level, int *subsample_level, unsigned long *start_timeout, PyObject **subscription) {
static char *kwlist[] = {"name", "address", "password", "encoding", "quality_level", "compress_level", "fine_quality_level", "subsample_level", "start_timeout", "subscription", NULL};
return PyArg_ParseTupleAndKeywords(args, kwds, "ss|ssiiiikO", kwlist, name, address, password, encoding, quality_level, compress_level, fine_quality_level, subsample_level, start_timeout, subscription);
}
static int PyArg_ParseTuple_close(PyObject *args, PyObject *kwds, char **name) {
static char *kwlist[] = {"name", NULL};
*name = "";
return PyArg_ParseTupleAndKeywords(args, kwds, "|s", kwlist, name);
}
static int PyArg_ParseTuple_name(PyObject *args, PyObject *kwds, char **name) {
static char *kwlist[] = {"name", NULL};
return PyArg_ParseTupleAndKeywords(args, kwds, "s", kwlist, name);
}
static int PyArg_ParseTuple_render(PyObject *args, PyObject *kwds, char **name, int *close) {
static char *kwlist[] = {"name", "close", NULL};
return PyArg_ParseTupleAndKeywords(args, kwds, "s|i", kwlist, name, close);
}
static int PyArg_ParseTuple_update(PyObject *args, PyObject *kwds, char **name, PyObject **subscription) {
static char *kwlist[] = {"name", "subscription", NULL};
return PyArg_ParseTupleAndKeywords(args, kwds, "sO", kwlist, name, subscription);
}
static PyObject *PyObject_CallFunctionObjArgs_1(PyObject *callable, PyObject *a) {
return PyObject_CallFunctionObjArgs(callable, a, NULL);
}
typedef struct {
PyObject_HEAD
PyObject *addresses;
} go_vncdriver_VNCSession_object;
// Can't access macros through cgo
static void go_vncdriver_decref(PyObject *obj) {
Py_DECREF(obj);
}
static void go_vncdriver_incref(PyObject *obj) {
Py_INCREF(obj);
}
*/
import "C"
import (
"fmt"
"os"
"os/exec"
"strconv"
"sync"
"time"
"unsafe"
"github.com/juju/errors"
logging "github.com/op/go-logging"
"github.com/openai/go-vncdriver/gymvnc"
"github.com/openai/go-vncdriver/vncclient"
)
var (
log = logging.MustGetLogger("go_vncdriver")
Py_None = &C._Py_NoneStruct
emptyString = C.CString("")
vncUpdatesN *C.PyObject
vncUpdatesPixels *C.PyObject
vncUpdatesRectangles *C.PyObject
vncUpdatesBytes *C.PyObject
setup sync.Once
)
func init() {
if os.Getenv("DEBUG_GO_VNCDRIVER_SO") != "" {
out, err := exec.Command("lsof", "-p", strconv.Itoa(os.Getpid())).CombinedOutput()
if err == nil {
fmt.Printf("%s\n", out)
} else {
fmt.Println("Error calling lsof: %v", err)
}
}
}
func setupOnce() {
// Must hold the GIL when we init these. Thus don't need
// Go-level locking as well.
vncUpdatesN = C.PyUnicode_FromString(C.CString("stats.vnc.updates.n"))
vncUpdatesPixels = C.PyUnicode_FromString(C.CString("stats.vnc.updates.pixels"))
vncUpdatesRectangles = C.PyUnicode_FromString(C.CString("stats.vnc.updates.rectangles"))
vncUpdatesBytes = C.PyUnicode_FromString(C.CString("stats.vnc.updates.bytes"))
gymvnc.ConfigureLogging()
}
//export GoVNCDriver_VNCSession_c_init
func GoVNCDriver_VNCSession_c_init(self *C.go_vncdriver_VNCSession_object) C.int {
setup.Do(setupOnce)
batch := gymvnc.NewVNCBatch()
info := sessionInfo{
batch: batch,
names: map[string]*C.char{},
screenNumpy: map[string]map[*gymvnc.Screen]*C.PyObject{},
}
if !info.preallocatePythonObjects() {
return C.int(-1)
}
batchLock.Lock()
defer batchLock.Unlock()
ptr := uintptr(unsafe.Pointer(self))
batchMgr[ptr] = &info
return C.int(0)
}
//export GoVNCDriver_VNCSession_connect
func GoVNCDriver_VNCSession_connect(self, args, kwds *C.PyObject) *C.PyObject {
batchLock.Lock()
defer batchLock.Unlock()
// Just store this as a property on self?
ptr := uintptr(unsafe.Pointer(self))
info, ok := batchMgr[ptr]
if !ok {
setError(errors.New("VNCSession is already closed"))
return nil
}
_ = info
nameC := new(*C.char)
addressC := new(*C.char)
passwordC := new(*C.char)
encodingC := new(*C.char)
qualityLevelC := new(C.int)
compressLevelC := new(C.int)
fineQualityLevelC := new(C.int)
subsampleLevelC := new(C.int)
startTimeoutC := new(C.ulong)
subscriptionPy := new(*C.PyObject)
*compressLevelC = C.int(-1)
*qualityLevelC = C.int(-1)
*fineQualityLevelC = C.int(-1)
*subsampleLevelC = C.int(-1)
if C.PyArg_ParseTuple_connect(args, kwds, nameC, addressC, passwordC, encodingC, qualityLevelC, compressLevelC, fineQualityLevelC, subsampleLevelC, startTimeoutC, subscriptionPy) == 0 {
return nil
}
name := C.GoString(*nameC)
address := C.GoString(*addressC)
password := C.GoString(*passwordC)
encoding := C.GoString(*encodingC)
qualityLevel := int(*qualityLevelC)
compressLevel := int(*compressLevelC)
fineQualityLevel := int(*fineQualityLevelC)
subsampleLevel := int(*subsampleLevelC)
startTimeout := int(*startTimeoutC)
subscription, ok := convertSubscriptionPy(*subscriptionPy)
if !ok {
return nil
}
if _, ok := info.names[name]; ok {
log.Infof("disconnecting existing connection %s", name)
info.close(name)
}
if password == "" {
// our default password!
password = "openai"
}
err := info.batch.Open(name, gymvnc.VNCSessionConfig{
Address: address,
Password: password,
Encoding: encoding,
QualityLevel: qualityLevel,
CompressLevel: compressLevel,
FineQualityLevel: fineQualityLevel,
SubsampleLevel: subsampleLevel,
StartTimeout: time.Duration(startTimeout) * time.Second,
Subscription: subscription,
})
if err != nil {
setError(err)
return nil
}
info.open(name)
// errCh := make(chan error, 10)
// done := make(chan bool)
// batch, err := gymvnc.NewVNCSession(, )
// if err != nil {
// close(done)
// setError(err)
// return C.int(-1)
// }
C.go_vncdriver_incref(Py_None)
return Py_None
}
//export GoVNCDriver_VNCSession_step
func GoVNCDriver_VNCSession_step(self, actionDict *C.PyObject) (rep *C.PyObject) {
// defer func() {
// if r := recover(); r != nil {
// stack := debug.Stack()
// setError(stack)
// rep = nil
// }
// }()
batchLock.Lock()
defer batchLock.Unlock()
ptr := uintptr(unsafe.Pointer(self))
info, ok := batchMgr[ptr]
if !ok {
setError(errors.New("VNCSession is closed"))
return nil
}
// Iterate over all active sessions
batchEvents := map[string][]gymvnc.VNCEvent{}
ok = true
for name, nameC := range info.names {
events := []gymvnc.VNCEvent{}
action := C.PyDict_GetItemString(actionDict, nameC)
if C.PyErr_Occurred() != nil {
return nil
}
if action != nil {
// Iterate over all events in the action
eventsIter := C.PyObject_GetIter(action)
for eventPy := C.PyIter_Next(eventsIter); ok && eventPy != nil; eventPy = C.PyIter_Next(eventsIter) {
event, convertOk := convertEventPy(eventPy)
if !convertOk {
ok = false
}
C.go_vncdriver_decref(eventPy)
events = append(events, event)
}
C.go_vncdriver_decref(eventsIter)
}
if !ok {
if C.PyErr_Occurred() == nil {
setError(errors.Errorf("BUG: unpacking actions failed for %s, but no Python error was set", name))
return nil
}
return nil
}
batchEvents[name] = events
}
// Put together the Python objects
observationN, updatesN, errN := info.batch.Step(batchEvents)
if ok := info.populateScreenPyDict(observationN); !ok {
return nil
}
if ok := info.populateInfoPyDict(updatesN); !ok {
return nil
}
if ok := info.populateErrorPyDict(errN); !ok {
return nil
}
// Actually, let's make this the user's responsibility, so
// they don't have disappearing names.
//
// // Cleanup anything that broke
// for name, err := range errN {
// if err != nil {
// info.close(name)
// }
// }
// Ownership will transfer away when we return
C.go_vncdriver_incref(info.screenInfoErrPytuple)
return info.screenInfoErrPytuple
}
//export GoVNCDriver_VNCSession_update
func GoVNCDriver_VNCSession_update(self, args, kwds *C.PyObject) *C.PyObject {
batchLock.Lock()
defer batchLock.Unlock()
ptr := uintptr(unsafe.Pointer(self))
info, ok := batchMgr[ptr]
if !ok {
setError(errors.New("VNCSession is closed"))
return nil
}
nameC := new(*C.char)
subscriptionPy := new(*C.PyObject)
if C.PyArg_ParseTuple_update(args, kwds, nameC, subscriptionPy) == 0 {
return nil
}
name := C.GoString(*nameC)
subscription, ok := convertSubscriptionPy(*subscriptionPy)
if !ok {
return nil
}
err := info.batch.SetSubscription(name, subscription)
if err != nil {
setError(err)
return nil
}
C.go_vncdriver_incref(Py_None)
return Py_None
}
var (
batchMgr = map[uintptr]*sessionInfo{}
batchLock sync.Mutex
)
func unpackStrings(strings *C.PyObject) ([]string, error) {
strs := []string{}
size := C.PyList_Size(strings)
for i := 0; i < int(size); i++ {
// Look at the i'th item and convert to a Go string
listItem := C.PyList_GetItem(strings, C.Py_ssize_t(i))
if listItem == nil {
return nil, errors.New("list lookup failed")
}
unicodeObj := C.PyUnicode_FromObject(listItem)
if unicodeObj == nil {
return nil, errors.New("could not convert item to unicode")
}
byteObj := C.PyUnicode_AsASCIIString(unicodeObj)
if byteObj == nil {
return nil, errors.New("could not convert unicode to ascii string")
}
strObj := C.PyBytes_AsString(byteObj)
if strObj == nil {
return nil, errors.New("could not convert bytes to C string")
}
str := C.GoString(strObj)
strs = append(strs, str)
}
return strs, nil
}
//export GoVNCDriver_VNCSession_render
func GoVNCDriver_VNCSession_render(self, args, kwds *C.PyObject) *C.PyObject {
batchLock.Lock()
defer batchLock.Unlock()
if !compiledWithGL {
setError(errors.New("go_vncdriver was installed without OpenGL support. See https://github.com/openai/go-vncdriver for details on how debug."))
return nil
}
// parse name argument
nameC := new(*C.char)
closeC := new(C.int)
if C.PyArg_ParseTuple_render(args, kwds, nameC, closeC) == 0 {
return nil
}
name := C.GoString(*nameC)
close := *closeC != C.int(0)
ptr := uintptr(unsafe.Pointer(self))
info, ok := batchMgr[ptr]
if !ok {
setError(errors.New("VNCSession is already closed"))
return nil
}
if err := info.initRenderer(name); err != nil {
setError(err)
return nil
}
err := info.batch.Render(name, close)
if err != nil {
// reportBestError(info.batch, err)
setError(err)
return nil
}
C.go_vncdriver_incref(Py_None)
return Py_None
}
// func reportBestError(batch *gymvnc.VNCBatch, err error) {
// var report string
// checkErr := batch.Check()
// if checkErr != nil {
// report = fmt.Sprintf("Error: %s\n\nOriginal error: %s", errors.ErrorStack(err), errors.ErrorStack(checkErr))
// } else {
// report = fmt.Sprintf("Error: %s", errors.ErrorStack(err))
// }
// setError(report)
// }
func GoString_FromPyString(t *C.PyObject) (string, bool) {
unicodePystr := C.PyUnicode_FromObject(t)
if unicodePystr == nil {
setError(errors.New("could not convert to unicode"))
return "", false
}
bytePystr := C.PyUnicode_AsASCIIString(unicodePystr)
if bytePystr == nil {
setError(errors.New("could not encode to ascii"))
return "", false
}
typePystr := C.PyBytes_AsString(bytePystr)
if typePystr == nil {
setError(errors.New("could not convert bytes to cstring"))
return "", false
}
return C.GoString(typePystr), true
}
func convertSubscriptionPy(subscriptionPy *C.PyObject) (regions []gymvnc.Region, ok bool) {
ok = true
if subscriptionPy == nil {
return
}
// Iterate over all subscription
subscriptionIter := C.PyObject_GetIter(subscriptionPy)
if subscriptionIter == nil {
ok = false
return
}
for itemPy := C.PyIter_Next(subscriptionIter); ok && itemPy != nil; itemPy = C.PyIter_Next(subscriptionIter) {
var x, y, width, height int
if ok {
x, ok = getIntFromTuple(itemPy, 0)
}
if ok {
width, ok = getIntFromTuple(itemPy, 1)
}
if ok {
y, ok = getIntFromTuple(itemPy, 2)
}
if ok {
height, ok = getIntFromTuple(itemPy, 3)
}
C.go_vncdriver_decref(itemPy)
regions = append(regions, gymvnc.Region{
X: uint16(x),
Y: uint16(y),
Width: uint16(width),
Height: uint16(height),
})
}
C.go_vncdriver_decref(subscriptionIter)
return
}
// Sets python error
func convertEventPy(eventPy *C.PyObject) (event gymvnc.VNCEvent, ok bool) {
// eventPy: ("PointerEvent", x, y, buttonmask) or ("KeyEvent", key, down)
// if PyTuple_Check(eventPy) == 0 {
// setError(errors.New("event was not a tuple"))
// ok = false
// return
// }
t := C.PyTuple_GetItem(eventPy, C.Py_ssize_t(0))
if t == nil {
C.PyErr_Clear()
repr, isOk := PyObject_Repr(eventPy)
if !isOk {
return
}
setError(errors.Errorf("Expected non-empty tuple rather than: %s", repr))
return
}
eventType, isOk := GoString_FromPyString(t)
if !isOk {
return
}
if eventType == "PointerEvent" {
x, isOk := getIntFromTuple(eventPy, 1)
if !isOk {
return
}
y, isOk := getIntFromTuple(eventPy, 2)
if !isOk {
return
}
mask, isOk := getIntFromTuple(eventPy, 3)
if !isOk {
return
}
event = gymvnc.PointerEvent{
Mask: vncclient.ButtonMask(mask),
X: uint16(x),
Y: uint16(y),
}
} else if eventType == "KeyEvent" {
keysym, isOk := getIntFromTuple(eventPy, 1)
if !isOk {
return
}
down, isOk := getBoolFromTuple(eventPy, 2)
if !isOk {
return
}
event = gymvnc.KeyEvent{
Keysym: uint32(keysym),
Down: down,
}
} else {
setError(errors.Errorf("invalid event type: %s", eventType))
}
ok = true
return
}
func getIntFromTuple(eventPy *C.PyObject, i int) (int, bool) {
iPyint := C.PyTuple_GetItem(eventPy, C.Py_ssize_t(i))
if iPyint == nil {
return 0, false
}
tup := C.PyLong_AsLong(iPyint)
if tup == -1 && C.PyErr_Occurred() != nil {
return 0, false
}
return int(tup), true
}
func getBoolFromTuple(eventPy *C.PyObject, i int) (bool, bool) {
iPyint := C.PyTuple_GetItem(eventPy, C.Py_ssize_t(i))
if iPyint == nil {
return false, false
}
t := C.PyObject_IsTrue(iPyint)
return t == 1, t != -1
}
func PyObject_Repr(obj *C.PyObject) (string, bool) {
res := C.PyObject_Repr(obj)
if res == nil {
return "", false
}
unicodeObj := C.PyUnicode_FromObject(res)
if unicodeObj == nil {
return "", false
}
byteObj := C.PyUnicode_AsASCIIString(unicodeObj)
if byteObj == nil {
return "", false
}
strObj := C.PyBytes_AsString(byteObj)
if strObj == nil {
return "", false
}
str := C.GoString(strObj)
return str, true
}
//export GoVNCDriver_VNCSession_close
func GoVNCDriver_VNCSession_close(self, args, kwds *C.PyObject) *C.PyObject {
nameC := new(*C.char)
if C.PyArg_ParseTuple_close(args, kwds, nameC) == 0 {
return nil
}
name := C.GoString(*nameC)
if name == "" {
log.Debug("closing entire VNCSession")
cast := (*C.go_vncdriver_VNCSession_object)(unsafe.Pointer(self))
GoVNCDriver_VNCSession_c_dealloc(cast)
} else {
log.Debugf("closing %s connection in VNCSession", name)
batchLock.Lock()
defer batchLock.Unlock()
ptr := uintptr(unsafe.Pointer(self))
info, ok := batchMgr[ptr]
if !ok {
setError(errors.New("VNCSession is already closed"))
return nil
}
info.close(name)
}
C.go_vncdriver_incref(Py_None)
return Py_None
}
//export GoVNCDriver_VNCSession_c_dealloc
func GoVNCDriver_VNCSession_c_dealloc(self *C.go_vncdriver_VNCSession_object) {
batchLock.Lock()
defer batchLock.Unlock()
ptr := uintptr(unsafe.Pointer(self))
info, ok := batchMgr[ptr]
if !ok {
return
}
for name := range info.names {
info.close(name)
}
var gstate C.PyGILState_STATE
gstate = C.PyGILState_Ensure()
defer C.PyGILState_Release(gstate)
if info.screenInfoErrPytuple != nil {
C.go_vncdriver_decref(info.screenInfoErrPytuple)
}
for _, screenToNumpy := range info.screenNumpy {
for _, screen := range screenToNumpy {
C.go_vncdriver_decref(screen)
}
}
// We do not own references to screenPyDict / infoPyDict / errPyDict
delete(batchMgr, ptr)
}
func setError(err error) {
C.PyErr_SetGoVNCDriverError(C.CString(errors.ErrorStack(err)))
}
type sessionInfo struct {
batch *gymvnc.VNCBatch
names map[string]*C.char
// return type: {...}, {...}, {...}
screenPyDict *C.PyObject
infoPyDict *C.PyObject
errPyDict *C.PyObject
screenInfoErrPytuple *C.PyObject
screenNumpy map[string]map[*gymvnc.Screen]*C.PyObject
rendererSet bool
}
func (b *sessionInfo) open(name string) {
b.names[name] = C.CString(name)
b.screenNumpy[name] = map[*gymvnc.Screen]*C.PyObject{}
}
func (b *sessionInfo) close(name string) {
b.batch.Close(name)
nameC := b.names[name]
C.free(unsafe.Pointer(nameC))
delete(b.names, name)
for _, screenNumpy := range b.screenNumpy[name] {
C.go_vncdriver_decref(screenNumpy)
}
delete(b.screenNumpy, name)
}
// Sets the Python error for you
func (b *sessionInfo) populateScreenPyDict(screens map[string]*gymvnc.Screen) bool {
C.PyDict_Clear(b.screenPyDict)
for name, screen := range screens {
var ary *C.PyObject
if screen != nil {
var ok bool
screenToNumpy := b.screenNumpy[name]
ary, ok = screenToNumpy[screen]
if !ok {
// allocate a new screen object, once
dims := []C.npy_intp{C.npy_intp(screen.Height), C.npy_intp(screen.Width), 3}
ary = C.GoPyArray_SimpleNewFromData(3, &dims[0], C.NPY_UINT8, unsafe.Pointer(&screen.Data[0]))
screenToNumpy[screen] = ary
}
} else {
ary = Py_None
}
if C.PyDict_SetItemString(b.screenPyDict, b.names[name], ary) == C.int(-1) {
return false
}
}
return true
}
func (b *sessionInfo) populateInfoPyDict(updateN map[string][]*vncclient.FramebufferUpdateMessage) bool {
C.PyDict_Clear(b.infoPyDict)
for name, update := range updateN {
dict := C.PyDict_New()
// Put the new dictionary into the info dict we're returning
ok := C.PyDict_SetItemString(b.infoPyDict, b.names[name], dict)
C.go_vncdriver_decref(dict)
if ok != C.int(0) {
return false
}
// Retain our reference!
// C.go_vncdriver_incref(vncUpdatesN)
updatePy := C.PyLong_FromLong(C.long(len(update)))
ok = C.PyDict_SetItem(dict, vncUpdatesN, updatePy)
C.go_vncdriver_decref(updatePy)
if ok != C.int(0) {
return false
}
// Count up number of pixels changed
pixels := 0
rectangles := 0
bytes := 0 // TODO: should we just compute this from the byte stream directly?
for _, updateI := range update {
for _, rect := range updateI.Rectangles {
pixels += int(rect.Width) * int(rect.Height)
rectangles++
// Each rectangle consists of X, Y,
// Width, Height (each 2 bytes) and an
// encoding.
//
// Technically, we should consider
// including other control messages in
// bytes, but this is ok for now.
bytes += rect.Enc.Size() + 8
}
}
// C.go_vncdriver_incref(vncUpdatesPixels)
pixelsPy := C.PyLong_FromLong(C.long(pixels))
ok = C.PyDict_SetItem(dict, vncUpdatesPixels, pixelsPy)
C.go_vncdriver_decref(pixelsPy)
if ok != C.int(0) {
return false
}
// C.go_vncdriver_incref(vncUpdatesRectangles)
rectsPy := C.PyLong_FromLong(C.long(rectangles))
ok = C.PyDict_SetItem(dict, vncUpdatesRectangles, rectsPy)
C.go_vncdriver_decref(rectsPy)
if ok != C.int(0) {
return false
}
// C.go_vncdriver_incref(vncUpdatesBytes)
bytesPy := C.PyLong_FromLong(C.long(bytes))
ok = C.PyDict_SetItem(dict, vncUpdatesBytes, bytesPy)
C.go_vncdriver_decref(bytesPy)
if ok != C.int(0) {
return false
}
}
return true
}
func (b *sessionInfo) populateErrorPyDict(errN map[string]error) bool {
C.PyDict_Clear(b.errPyDict)
for name, err := range errN {
if err != nil {
errC := C.CString(err.Error())
errPy := C.PyUnicode_FromString(errC)
C.free(unsafe.Pointer(errC))
if errPy == nil {
return false
}
ok := C.PyDict_SetItemString(b.errPyDict, b.names[name], errPy)
C.go_vncdriver_decref(errPy)
if ok != C.int(0) {
return false
}
}
}
return true
}
// Preallocate all needed Python objects, so we don't need to generate
// a lot of garbage. (In practice the speedup here might be minimal,
// but does save a bit of code complexity.
func (b *sessionInfo) preallocatePythonObjects() bool {
b.screenPyDict = C.PyDict_New()
if b.screenPyDict == nil {
return false
}
b.infoPyDict = C.PyDict_New()
if b.infoPyDict == nil {
return false
}
b.errPyDict = C.PyDict_New()
if b.errPyDict == nil {
return false
}
b.screenInfoErrPytuple = C.PyTuple_New(C.Py_ssize_t(3))
if b.screenInfoErrPytuple == nil {
return false
}
if i := C.PyTuple_SetItem(b.screenInfoErrPytuple, C.Py_ssize_t(0), b.screenPyDict); i != C.int(0) {
return false
}
if i := C.PyTuple_SetItem(b.screenInfoErrPytuple, C.Py_ssize_t(1), b.infoPyDict); i != C.int(0) {
return false
}
if i := C.PyTuple_SetItem(b.screenInfoErrPytuple, C.Py_ssize_t(2), b.errPyDict); i != C.int(0) {
return false
}
return true
}
func main() {
}