Go

Let's create a Go application using the NYRA framework.

Creating the NYRA App

To begin, we will construct a Nyra Go application by integrating several pre-configured Nyra packages. Proceed with the following steps:

>_Terminal
tman install app default_app_go
cd default_app_go

tman install protocol msgpack
tman install extension_group default_extension_group

Installing a Default NYRA Extension

Next, install a default NYRA extension written in Go:

_Terminal
tman install extension default_extension_go

Declaring the Prebuilt Graph for Auto-Start

Next, we will update the property.json file of the Nyra application to incorporate a graph declaration. This configuration ensures that the default extension initializes automatically upon launching the Nyra application.

.json
"predefined_graphs": [
  {
    "name": "default",
    "auto_start": true,
    "nodes": [
      {
        "type": "extension_group",
        "name": "default_extension_group",
        "addon": "default_extension_group"
      },
      {
        "type": "extension",
        "name": "default_extension_go",
        "addon": "default_extension_go",
        "extension_group": "default_extension_group"
      }
    ]
  }
]

Building the App

Unlike conventional Go projects, the Nyra Go application leverages CGo, requiring the configuration of specific environment variables prior to the build process. To simplify this, the Nyra runtime Go binding system package includes a pre-configured build script, enabling the application to be compiled with a single command.

>_Terminal
go run nyra_packages/system/nyra_runtime_go/tools/build/main.go

The compiled binary, main, will be generated in the /bin folder.

Starting the App

Since some environment variables need to be set, it is recommended to start the app using the provided script:

>_Terminal
./bin/start

Debugging

If Visual Studio Code (VSCode) is your chosen development environment, you can apply the following configurations to facilitate debugging for Go and C code.

Debugging Go Code

.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Go app (launch)",
            "type": "go",
            "request": "launch",
            "mode": "auto",
            "program": "${workspaceFolder}",
            "cwd": "${workspaceFolder}",
            "output": "${workspaceFolder}/bin/tmp",
            "env": {
                "LD_LIBRARY_PATH": "${workspaceFolder}/lib",
                "DYLD_LIBRARY_PATH": "${workspaceFolder}/lib",
                "CGO_LDFLAGS": "-L${workspaceFolder}/lib -lnyra_runtime_go -Wl,-rpath,@loader_path/lib"
            }
        }
    ]
}

Debugging C Code

.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "C (launch)",
            "type": "lldb",
            "request": "launch",
            "program": "${workspaceFolder}/bin/main",
            "cwd": "${workspaceFolder}",
            "env": {
                "LD_LIBRARY_PATH": "${workspaceFolder}/lib",
                "DYLD_LIBRARY_PATH": "${workspaceFolder}/lib",
                "CGO_LDFLAGS": "-L${workspaceFolder}/lib -lnyra_runtime_go -Wl,-rpath,@loader_path/lib"
            }
        }
    ]
}

CGO

Generated Code

When integrating Go with C, the cgo tool plays a pivotal role. It facilitates the interaction by translating Go source files into multiple output files, encompassing both Go and C source code. The resulting C source files are subsequently compiled using tools such as gcc or clang.

To generate these C and Go source files, execute the following command:

>_Terminal
mkdir out
go tool cgo -objdir out

Example:

_Terminal
go tool cgo -objdir out cmd.go error.go

The generated files will include:

.bash
β”œβ”€β”€ _cgo_export.c
β”œβ”€β”€ _cgo_export.h
β”œβ”€β”€ _cgo_flags
β”œβ”€β”€ _cgo_gotypes.go
β”œβ”€β”€ _cgo_main.c
β”œβ”€β”€ _cgo_.o
β”œβ”€β”€ cmd.cgo1.go
β”œβ”€β”€ cmd.cgo2.c
β”œβ”€β”€ error.cgo1.go
└── error.cgo2.c

_cgo_export.h serves as a critical component in facilitating interoperability between Go and C within the cgo framework. This header file contains the requisite declarations for Go functions that are accessible from C, acting as a bridge between the two languages.

The file is automatically generated based on Go functions explicitly marked with the //export directive. It also incorporates type definitions that map C types to their corresponding Go types, ensuring seamless compatibility and communication between Go and C during function calls.

Example of type definitions:

.h
typedef signed har GoInt8;
typedef unsigned char GoUint8;
typedef short GoInt16;
typedef unsigned short GoUint16;
typedef int GoInt32;
typedef unsigned int GoUint32;
typedef long long GoInt64;
typedef unsigned long long GoUint64;
typedef GoInt64 GoInt;
typedef GoUint64 GoUint;
typedef size_t GoUintptr;
typedef float GoFloat32;
typedef double GoFloat64;
_cgo_gotypes.go contains the corresponding Go types defined in C and used in Go.

For example, if you define a struct nyra_go_error_t in a header file common.h and use C.nyra_go_error_t in Go, there will be a corresponding Go type in _cgo_gotypes.go:

.go
package nyra

type _Ctype_nyra_go_status_t = _Ctype_struct_nyra_go_status_t

type _Ctype_struct_nyra_go_status_t struct {
    err_no   _Ctype_int
    msg_size _Ctype_uint8_t
    err_msg  [256]_Ctype_char
    _        [3]byte
}

cmd.cgo1.go is a cgo-generated derivative of the original Go source file, cmd.go. This file replaces direct invocations of C functions and types with intermediary Go functions and types generated by cgo. These abstractions streamline the interaction between Go and C, ensuring proper type safety and function mapping.

Example:

.go
package nyra

func NewCmd(cmdName string) (Cmd, error) {
    // ...
    cStatus := func() _Ctype_struct_nyra_go_status_t {
        var _cgo0 _cgo_unsafe.Pointer = unsafe.Pointer(unsafe.StringData(cmdName))
        var _cgo1 _Ctype_int = _Ctype_int(len(cmdName))
        _cgoBase2 := &bridge
        _cgo2 := _cgoBase2
        _cgoCheckPointer(_cgoBase2, 0 == 0)
        return _Cfunc_ten_go_cmd_create_custom_cmd(_cgo0, _cgo1, _cgo2)
    }()
    // ...
}

cmd.cgo2.c is a wrapper of the original C function called from Go.

Example:

.c
CGO_NO_SANITIZE_THREAD void _cgo_cb1b98e39356_Cfunc_nyra_go_cmd_create_custom_cmd(void *v) {
    struct {
        const void* p0;
        int p1;
        char __pad12[4];
        nyra_go_msg_t** p2;
        nyra_go_error_t r;
    } __attribute__((__packed__, __gcc_struct__)) *_cgo_a = v;
    char *_cgo_stktop = _cgo_topofstack();
    __typeof__(_cgo_a->r) _cgo_r;
    _cgo_tsan_acquire();
    _cgo_r = nyra_go_cmd_create_cmd(_cgo_a->p0, _cgo_a->p1, _cgo_a->p2);
    _cgo_tsan_release();
    _cgo_a = (void*)((char*)_cgo_a + (_cgo_topofstack() - _cgo_stktop));
    _cgo_a->r = _cgo_r;
    _cgo_msan_write(&_cgo_a->r, sizeof(_cgo_a->r));
}

So, the calling sequence of C.nyra_go_cmd_create_cmd() from Go is:

_Cfunc_nyra_go_cmd_create_custom_cmd (auto-generated Go)
           |
           V
   _cgo_runtime_cgocall (Go)
           |
           V
      entrysyscall     // switch Go stack to C stack
           |
           V
_cgo_cb1b98e39356_Cfunc_nyra_go_cmd_create_custom_cmd (auto-generated C)
           |
           V
   nyra_go_cmd_create_cmd (C)
           |
           V
      exitsyscall

Incomplete Type

As previously outlined, the cgo tool generates corresponding Go type definitions in the _cgo_gotypes.go file by interpreting the C header files imported using import "C". For C structures declared as opaque within the header files, cgo produces an incomplete type representation in the generated Go code to maintain consistency with the abstraction.

Example of an opaque C structure:

.h
typedef struct nyra_go_msg_t nyra_go_msg_t;

The cgo tool will generate an incomplete type in Go:

.go
type _Ctype_nyra_go_msg_t = _Ctype_struct_nyra_go_msg_t
type _Ctype_struct_nyra_go_msg_t _cgopackage.Incomplete

What happens if you use the incomplete type in Go?

Incomplete types are inherently non-allocatable.

As a result, structures such as sys.NotInHeap cannot reside on the Go heap, rendering operations like new or makeinapplicable. Any attempt to instantiate an opaque struct within Go will trigger a compiler error:

msg := new(C.nyra_go_msg_t) // Error: can't be allocated in Go; it is incomplete (or unallocatable)

Pointers to incomplete types cannot be directly passed to C functions.

When a C function accepts a pointer to an opaque struct as a parameter, cgo prohibits passing a Go pointer to the corresponding incomplete type directly. This restriction arises from the need to comply with Go's garbage collection (GC) rules. The Go compiler mandates that such pointers be "pinned" to ensure their memory locations remain stable during interaction with C code.

Rules for Using C.uintptr_t Instead of a Pointer to an Opaque Struct

The use of C.uintptr_t in C anduintptr in Go provides a mechanism to represent integers large enough to store a C pointer. This eliminates the need for memory allocation or complex conversions when passing data between Go and C, ensuring efficient interoperability.

Constraints:

  1. Lack of Pointer Semantics: uintptrin Go is an integer type that represents the raw bit pattern of an address but does not exhibit pointer semantics. Consequently, pointer arithmetic or operations associated with true pointers are not supported.

  2. Non-Dereferenceable in Go: uintptrcannot be directly dereferenced in Go. Converting it to an unsafe.Pointer introduces potential risks, particularly concerning Go’s garbage collector (GC), which may relocate or reclaim memory, leading to undefined behavior.

  3. Null Representation: Since uintptris treated as an integer, it cannot directly represent a null pointer (e.g., nil or NULL). Instead, the value 0 must be used explicitly to denote a null address when interfacing with C code.

These characteristics necessitate caution when using uintptr, especially in scenarios involving Go’s memory management and type safety constraints.

Last updated