How to pass Go instances between Go and C functions?

89 Views Asked by At

I want to pass a Go instance as an argument to each call from C, like the code below.

I want to merely operate pointers to http.Client instances as opaque pointers in my C code.

I have a few questions.

  • How to convert &http.Client to C type?
  • How to prevent memory allocated by go from being freed?
  • How to convert C.HTTP_CLIENT_HDL type to &http.Client type?
  • How to free memory allocated by go?
typedef size_t HTTP_CLIENT_HDL

HTTP_CLIENT_HDL client_hdl = http_new_client();
int res = http_post(client_hdl, ...);
http_free_client(client_hdl);
/*
typedef size_t HTTP_CLIENT_HDL; // correct type?
*/
import "C"
import (
    "net/http"
)

//export http_new_client
func http_new_client() (client_hdl C.HTTP_CLIENT_HDL){
    client := &http.Client{
        ...
    }
    
    return C.HTTP_CLIENT_HDL(client)    // Is correct?
                                        // How to prevent memory allocated to the client variable from being freed?
}

//export http_post
func http_post(client_hdl C.HTTP_CLIENT_HDL, ...) (ret_val C.int) {
    client := &http.Client???(client_hdl)   // how to convert C.HTTP_CLIENT_HDL to &http.Client?
    
    ...
    req, err := http.NewRequest("POST", ...)
    if err != nil {
        return 0
    }
    ...
    resp, err := client.Do(req)
    ...
    
    return 1
}

//export http_free_client
func http_free_client(client_hdl C.HTTP_CLIENT_HDL){
    client := &http.Client???(client_hdl)   // how to convert C.HTTP_CLIENT_HDL type to &http.Client type?
    
    free???(client)     // how to free the memory for client ?
}
1

There are 1 best solutions below

0
kostix On

Well, Volker is correct: cgo rules dictate that you cannot pass to the C side pointers (references) to Go-allocated memory if that memory contains Go pointers (see this). Well, the full ruleset is more complex but you'd better check the manual.

The rationale behind that is that even though no known contemporary implementation of Go employs moving garbage collector, the language spec does not preclude such implementation, which means the Go runtime powering your program must be able to know all the pointers to a particular Go-allocated memory chunk, and be able to update all of them if it so wishes.

So, a proper implementation would be to maintain a map of Go-allocated *http.Clients to some non-pointer "handles", and make the C code work with them–something like this:

package main

import (
    "fmt"
    "net/http"
    "os"
    "sync"
)

/*
#if __STDC_VERSION__ >= 199901L
    #include <stdint.h>
    typedef uint64_t HTTP_CLIENT_HDL;
#else
    typedef unsigned long int HTTP_CLIENT_HDL;
#endif

#include <stdio.h>

HTTP_CLIENT_HDL http_new_client(void);
void http_free_client(HTTP_CLIENT_HDL);
char* http_post(HTTP_CLIENT_HDL, char*);

static int do_something(void) {
    HTTP_CLIENT_HDL h;
    char *err;
    int rc;

    h = http_new_client();

    err = http_post(h, "http://localhost:8080/");
    if (err != NULL) {
        fprintf(stderr, "%s\n", err);
        free(err);
        rc = 0;
        goto cleanup;
    }

    rc = 1;
    printf("OK\n");

cleanup:
    http_free_client(h);

    return rc;
}
*/
import "C"

type httpClients struct {
    mu      sync.Mutex
    serial  C.HTTP_CLIENT_HDL
    clients map[C.HTTP_CLIENT_HDL]*http.Client
}

func (htc *httpClients) init() {
    htc.clients = make(map[C.HTTP_CLIENT_HDL]*http.Client)
}

func (htc *httpClients) new() C.HTTP_CLIENT_HDL {
    htc.mu.Lock()
    defer htc.mu.Unlock()

    c := &http.Client{}

    id := htc.serial
    htc.clients[id] = c
    htc.serial++

    return id
}

func (htc *httpClients) free(id C.HTTP_CLIENT_HDL) {
    htc.mu.Lock()
    defer htc.mu.Unlock()

    delete(htc.clients, id)
}

func (htc *httpClients) get(id C.HTTP_CLIENT_HDL) *http.Client {
    htc.mu.Lock()
    defer htc.mu.Unlock()

    c, ok := htc.clients[id]
    if !ok {
        panic(fmt.Sprintf("no http client with id %d", id))
    }

    return c
}

var htClients httpClients

func init() {
    htClients.init()
}

//export http_new_client
func http_new_client() C.HTTP_CLIENT_HDL {
    return htClients.new()
}

//export http_free_client
func http_free_client(h C.HTTP_CLIENT_HDL) {
    htClients.free(h)
}

//export http_post
func http_post(h C.HTTP_CLIENT_HDL, url *C.char) *C.char {
    c := htClients.get(h)

    req, err := http.NewRequest("POST", C.GoString(url), nil)
    if err != nil {
        return C.CString(err.Error())
    }

    _, err = c.Do(req)
    if err != nil {
        return C.CString(err.Error())
    }

    return nil
}

func main() {
    rc := C.do_something()
    if rc == 0 {
        os.Exit(1)
    }
}

Note that this code is not particularly effective as each call to http_* functions goes through a mutex protecting the map of "handles" to *http.Clients.


On a side note: do you really need to allocate http.Clients? The net/http code is supposed to either work with the default client, http.DefaultClient, or with a single non-default instance, configured with particular parameters. IOW, you do not need to allocate a new http.Client to perform an HTTP request using net/http, so maybe you're solving a non-problem to begin with.