Unbuffered input manager in Go? - go

I am creating a simple console-based game in Go. I want some way to accept unbuffered input (as in, you type in one key and it is immediately returned). I started out with this code:
func InitInput() {
exec.Command("stty", "-f", "/dev/tty", "cbreak", "min", "1").Run()
exec.Command("stty", "-f", "/dev/tty", "-echo").Run()
}
func StopInput() {
exec.Command("stty", "-f", "/dev/tty", "echo").Run()
}
func GetInput() string {
var b []byte = make([]byte, 1)
for {
os.Stdin.Read(b)
return string(b)
}
}
This was amazing, but it only works on a *nix-based os, and requires 3 functions. Next, someone recommended this code for me:
/*
// Works also for 64 bits
#ifdef _WIN32
// Lib for console management in windows
#include "conio.h"
#else
// Libs terminal management in Unix, Linux...
#include <stdio.h>
#include <unistd.h>
#include <termios.h>
// Implement reading a key pressed in terminal
char getch(){
char ch = 0;
struct termios old = {0};
fflush(stdout);
if( tcgetattr(0, &old) < 0 ) perror("tcsetattr()");
old.c_lflag &= ~ICANON;
old.c_lflag &= ~ECHO;
old.c_cc[VMIN] = 1;
old.c_cc[VTIME] = 0;
if( tcsetattr(0, TCSANOW, &old) < 0 ) perror("tcsetattr ICANON");
if( read(0, &ch,1) < 0 ) perror("read()");
old.c_lflag |= ICANON;
old.c_lflag |= ECHO;
if(tcsetattr(0, TCSADRAIN, &old) < 0) perror("tcsetattr ~ICANON");
return ch;
}
#endif
*/
import "C"
And then you only need 1 function:
func GetInput() string {
return string(byte(C.getch()))
}
This works perfectly, except that because of the way that cgo works, it is very slow which is not ideal for a game. Also, the original code for testing for a newline, if extras.GetInput() == "\n" {} doesn't work anymore. Is there a way to get a single-character unbuffered input manager to work in Go without using a big, thick, external library?

I wrote some console handling from scratch sources, in
C, Perl and PHP and was about to do one in Python.
Then I discovered Go and I'm asking the same questions.
I agree with the author of the thread, we have to do the minimum.
I haven't tried your code yet (The one which includes termio code).
There are many reasons why it may be slow:
1 / You make a C call every time you hit a key:
You may change this topology:
first, you place the console in non-buffering mode and return the saved parameters to
Golang.
2/ You hit keys according to this code:
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
// disable input buffering
exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run()
// do not display entered characters on the screen
exec.Command("stty", "-F", "/dev/tty", "-echo").Run()
// restore the echoing state when exiting
defer exec.Command("stty", "-F", "/dev/tty", "echo").Run()
var b []byte = make([]byte, 1)
LOOP:
for {
os.Stdin.Read(b)
var n uint8 = b[0]
switch n {
case 27, 113, 81 : fmt.Println("ESC, or 'q' or 'Q' was hitted!")
break LOOP
default:
fmt.Printf("You typed : %d\n", n)
}
}
}
Then at last you restore console settings.
In this code : I use stty but I suppose I could replace it with 2 C routine calls.
Well, I need to try to implement this...
Also, a question: should we return a byte, a char a widechar? This can be more complicated for sure.
Notice that you have also extended keys on keyboard and must read forward one or more bytes to get the whole sequence, but this is not a problem.

Related

Is it possible to detect if a writer is tty or not?

Bash has a 'magical behavior', if you type 'ls', usually you will get colorful output, but if you redirect the output to a file, the color codes are gone. How to achive this effect using Go. e.g. With the following statement:
fmt.Println("\033[1;34mHello World!\033[0m")
I can see the text in color, but if I pipe the output to a file, the color is preserved, which is NOT what I want.
BTW, this question is mostly not related to Go, I just want to achive the effect in my go program.
Bash has a 'magical behavior', if you type 'ls', usually you will
get colorful output, but if you redirect the output to a file, the
color codes are gone.
It's not Bash feature, it's ls feature. It calls
isatty()
to check if stdout file descriptor refers to a terminal. In musl libc
isatty is implemented like that:
int isatty(int fd)
{
struct winsize wsz;
unsigned long r = syscall(SYS_ioctl, fd, TIOCGWINSZ, &wsz);
if (r == 0) return 1;
if (errno != EBADF) errno = ENOTTY;
return 0;
}
You can use the same method in Go:
package main
import (
"fmt"
"os"
"golang.org/x/sys/unix"
)
func main() {
_, err := unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ)
if err != nil {
fmt.Println("Hello World")
} else {
fmt.Println("\033[1;34mHello World!\033[0m")
}
}

cgo calling share library: cannot find lib or function?

I'm using the sample code of chapter 13 of The Go Programming Language as below:
$ cat bzip2.c
#include <bzlib.h>
int bz2compress(bz_stream *s, int action,
char *in, unsigned *inlen, char *out, unsigned *outlen) {
s->next_in = in;
s->avail_in = *inlen;
s->next_out = out;
s->avail_out = *outlen;
int r = BZ2_bzCompress(s, action);
*inlen -= s->avail_in;
*outlen -= s->avail_out;
s->next_in = s->next_out = NULL;
return r;
}
$ cat usebzip2.go
// Package bzip provides a writer that uses bzip2 compression (bzip.org).
package main
import "C"
import (
"io"
"log"
"os"
"testing"
"unsafe"
)
type writer struct {
w io.Writer // underlying output stream
stream *C.bz_stream
outbuf [64 * 1024]byte
}
// Close flushes the compressed data and closes the stream.
// It does not close the underlying io.Writer.
func (w *writer) Close() error {
if w.stream == nil {
panic("closed")
}
defer func() {
C.BZ2_bzCompressEnd(w.stream)
C.bz2free(w.stream)
w.stream = nil
}()
for {
inlen, outlen := C.uint(0), C.uint(cap(w.outbuf))
r := C.bz2compress(w.stream, C.BZ_FINISH, nil, &inlen,
(*C.char)(unsafe.Pointer(&w.outbuf)), &outlen)
if _, err := w.w.Write(w.outbuf[:outlen]); err != nil {
return err
}
if r == C.BZ_STREAM_END {
return nil
}
}
}
// NewWriter returns a writer for bzip2-compressed streams.
func NewWriter(out io.Writer) io.WriteCloser {
const blockSize = 9
const verbosity = 0
const workFactor = 30
w := &writer{w: out, stream: C.bz2alloc()}
C.BZ2_bzCompressInit(w.stream, blockSize, verbosity, workFactor)
return w
}
func main() {
w := NewWriter(os.Stdout)
if _, err := io.Copy(w, os.Stdin); err != nil {
log.Fatalf("bzipper: %v\n", err)
}
if err := w.Close(); err != nil {
log.Fatalf("bzipper: close: %v\n", err)
}
}
First I compile the .c file:
gcc -I/usr/include -L/usr/lib -lbz2 --shared bzip2.c -fPIC -o libbzip2.so
The linux environment LD_LIBRARY_PATH contains ".", and then go build fails:
go build usebzip2.go
# command-line-arguments
/tmp/go-build677611698/b001/_x002.o: In function `_cgo_22d5d7fabfe4_Cfunc_bz2compress':
/tmp/go-build/cgo-gcc-prolog:118: undefined reference to `bz2compress'
collect2: error: ld returned 1 exit status
So how to fix it? I'm using ubuntu 18.04 LTS. Thanks a lot.
Don't run:
go build usebzip2.go
but rather:
go build
(and you don't need to invoke gcc directly on bzip2.c). When you use this process, you'll get many more (but different) errors because you have not put in the right directives before the:
import "C"
line. You need a comment (or series of comments) telling cgo about the functions you intend to provide, or providing those functions inline, and to direct the link phase to use -lbz2. In particular, you will need to:
#include <bzlib.h>
provide a bz2alloc function
provide a bz2free function
provide a declaration for your bz2compress function
set the LDFLAGS to include -lbz2
The actual bz2alloc and bz2free are short and simple and therefore can be included directly in this header block:
package main
/*
#cgo LDFLAGS: -lbz2
#include <bzlib.h>
#include <stdlib.h>
bz_stream *bz2alloc() { return calloc(1, sizeof(bz_stream)); }
int bz2compress(bz_stream *s, int action,
char *in, unsigned *intlen, char *out, unsigned *outlen);
void bz2free(bz_stream* s) { free(s); }
*/
import "C"
If you insert this and run go build you will now see a different and more useful error:
./usebzip2.go:60:2: cannot use w (type *writer) as type io.WriteCloser in return argument:
*writer does not implement io.WriteCloser (missing Write method)
which is of course because type writer does not implement Write.
(There's a completed version of exercise 13.3—not mine—at https://github.com/torbiak/gopl/tree/master/ex13.3. Note that they have augmented theirs to use locking as well, making it safe to call the write function from multiple goroutines simultaneously.)

golang get char* as return value from dll

I'm using golang to call a Dll function like char* fn(), the dll is not written by myself and I cannot change it. Here's my code:
package main
import (
"fmt"
"syscall"
"unsafe"
)
func main() {
dll := syscall.MustLoadDLL("my.dll")
fn := dll.MustFindProc("fn")
r, _, _ := fn.Call()
p := (*byte)(unsafe.Pointer(r))
// define a slice to fill with the p string
data := make([]byte, 0)
// loop until find '\0'
for *p != 0 {
data = append(data, *p) // append 1 byte
r += unsafe.Sizeof(byte(0)) // move r to next byte
p = (*byte)(unsafe.Pointer(r)) // get the byte value
}
name := string(data) // convert to Golang string
fmt.Println(name)
}
I have some questions:
Is there any better way of doing this? There're hundred of dll functions like this, I'll have to write the loop for all functions.
For very-long-string like 100k+ bytes, will append() cause performance issue?
Solved. the unsafe.Pointer(r) causes linter govet shows warning possible misuse of unsafe.Pointer, but the code runs fine, how to avoid this warning? Solution: This can be solved by adding -unsafeptr=false to govet command line, for vim-ale, add let g:ale_go_govet_options = '-unsafeptr=false'.
Casting uintptr as upointer is haram.
You must read the rules:
https://golang.org/pkg/unsafe/#Pointer
But there's hacky way, that shouldn't produce warning:
//go:linkname gostringn runtime.gostringn
func gostringn(p uintptr, l int) string
//go:linkname findnull runtime.findnull
//go:nosplit
func findnull(s uintptr) int
// ....
name := gostringn(r, findnull(r))
Functions takes pointer, but we link them from runtime as uintptr because they have same sizeof.
Might work in theory. But is also frowned upon.
Getting back to your code, as JimB said, you could do it one line with:
name := C.GoString((*C.char)(unsafe.Pointer(r)))
I got the following solution by tracking the os.Args of the go source code, But I am based on go1.17. If you are in another version, you can read the source code to solve it.
func UintPtrToString(r uintptr) string {
p := (*uint16)(unsafe.Pointer(r))
if p == nil {
return ""
}
n, end, add := 0, unsafe.Pointer(p), unsafe.Sizeof(*p)
for *(*uint16)(end) != 0 {
end = unsafe.Add(end, add)
n++
}
return string(utf16.Decode(unsafe.Slice(p, n)))
}

How can I detect OS version in Go

Relatively new to Go. Seems trivial, but I can't figure out how to detect the OS version. I know I can use runtime.GOOS and runtime.GOARCH to get the platform and architecture, but say I know I'm on linux but I want to find if I'm on RH6 vice RH7, etc. is what I'm trying to figure out.
So there's this obscure Uname method in the syscall package that basically does it, at least on Linux. The struct it fills is a bit clunky and undocumented, but you can get the gist of it:
import (
"fmt"
"syscall"
)
// A utility to convert the values to proper strings.
func int8ToStr(arr []int8) string {
b := make([]byte, 0, len(arr))
for _, v := range arr {
if v == 0x00 {
break
}
b = append(b, byte(v))
}
return string(b)
}
func main() {
var uname syscall.Utsname
if err := syscall.Uname(&uname); err == nil {
// extract members:
// type Utsname struct {
// Sysname [65]int8
// Nodename [65]int8
// Release [65]int8
// Version [65]int8
// Machine [65]int8
// Domainname [65]int8
// }
fmt.Println(int8ToStr(uname.Sysname[:]),
int8ToStr(uname.Release[:]),
int8ToStr(uname.Version[:]))
}
}
BTW This doesn't work on the playground, probably because of the sandbox limitations, but works on Linux. Haven't tested other systems.

How to return a slice in Go and calling from C?

I am trying to use cgo to use Go package in C code. Following is a piece of my code:
func LinearTransformToUInt8(frame []int64, winWidth int, winCenter int) []uint8 {
var transformed []uint8
// my cool code
return transformed
}
However, when calling from C, it says
panic: runtime error: cgo result has Go pointer
I believe the problem is the returned []uint8 is a Go type, which should be replaced by a C type. However, I don't know how to achieve it. Please help!
main.go
package main
import (
"C"
"unsafe"
)
import (
"reflect"
)
func main() {
}
//export phew
func phew() uintptr {
res := make([]uint8, 2)
for i := 0; i < 2; i++ {
res[i] = uint8(i + 1)
}
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&res))
return hdr.Data
}
main.c
#include <stdio.h>
#include <inttypes.h>
#include "libtemp.h"
int main(){
uintptr_t resPtr = phew();
uint8_t *res = (uint8_t*)resPtr;
for (int i = 0; i < 2; i++){
printf("%d\n", res[i]);
}
printf("Exiting gracefully\n");
}
You cannot pass a Go pointer which contains other Go Pointer, slice,string,channel,function, interface, map contain pointers.
So one cannot pass them around, rules to passing around pointers are documented here and go's representation of basic types is documented here.
But some Go contributors were saying, one shouldn't return a Go pointer to C code in the first place.

Resources