Search code examples
multithreadinggoc-api

Calling a Go callback from a threaded low-level C/C++ code layer in a Go application


I have a Go application and some C API function (e.g. some Win32 API function) that works asynchronously and spawns worker threads. This API function calls callbacks from some of those worker threads. The threads are system ones and are created internally by the C code (not by Go). Now, I want to pass Go functions as callbacks to that C API function. So, the Go callback functions would be called by the C function in the context of the worker threads, not known to the Go application.

We can assume that the safety measures have been taken and all the data access in callbacks is properly guarded by mutexes in order not to interfere with the main Go code.

The question is "Does Go support such scenario?", i.e. would callbacks work properly or something can easily crash inside because the Go runtime is not designed for what I'd like to do?


Solution

  • I have conducted the experiment of making a Go callback, called from 20 native Windows threads in parallel. The callback increments a variable, adds elements to the map and prints the value on the screen. Everything works smoothly, so I assume that there would be no problems in more complex scenarios as well.

    Here's the source code of my tests for others to use:

    proxy.h

    #ifndef _PROXY_H_
    #define _PROXY_H_
    long threaded_c_func(long param);
    #endif
    

    proxy.c

    #include "proxy.h"
    #ifdef WIN32
    #include <Windows.h>
    #endif
    
    #define ROUNDS 20
    
    volatile long passed = 0;
    
    extern long long threadedCallback(long cbidx);
    
    DWORD WINAPI ThreadFunc(LPVOID param) {    
        threadedCallback(*((long *)param));
        InterlockedIncrement(&passed);
    }
    
    long threaded_c_func(long cbidx) {
    
        for (int i  = 0; i < ROUNDS; i++)
        {
            DWORD ThreadId = 0;
            CreateThread(NULL, 1024*1024, &ThreadFunc, (LPVOID) &cbidx, 0, &ThreadId);
        }
        while (passed < ROUNDS)
        {
            Sleep(100);
        }
        return ROUNDS;
    } 
    

    callbackTest.go

    package main
    
    /*
    #cgo CFLAGS: -I .
    #cgo LDFLAGS: -L .
    
    #include "proxy.h"
    
    long threaded_c_func(long param);
    */
    import "C"
    
    import (
        "fmt"
        "strconv"
        "sync"
    )
    
    var hashTable map[int32]string
    
    var count int32
    var mtx sync.Mutex
    
    //export threadedCallback
    func threadedCallback(cbidx int) C.longlong {
        mtx.Lock()
        defer mtx.Unlock()
        count++
        hashTable[count] = strconv.Itoa(int(count))
        fmt.Println("Current counter ", count)
        return C.longlong(count)
    }
    
    func main() {
        hashTable = make(map[int32]string)
        var expected C.long
        expected = C.threaded_c_func(1)
        if int32(expected) == count {
            fmt.Println("Counters match")
        } else {
            fmt.Println("Expected ", int32(expected), " got ", count)
        }
        for k, v := range hashTable {
            if strconv.Itoa(int(k)) == v {
                fmt.Println(v, " match")
            } else {
                fmt.Println(v, "don't  match")
            }
        }
    }