We want to create an API to communicate witha device we currently sell. The API should be available for several platforms like C / C++ / .NET / Python and available for Windows and Linux. The idea is to have on single source of truth. A C API and the others are just wrappers around that C API.
However as i am not the greatest C programmer but i am very familiar with GO. GOs inbuilt runtime library also offers a lot of stuff, that makes implementing the functionality of the API a lot easier. The idea is to write the functionality in GO, and use CGO to create a C interface out of it.
So far i have started the functionality in GO and started writing a .NET wrapper in C# utilizing P/Invokes.
I would be interested in a bit of feedback in this approach. Especially how the interop / error result wrapping is done.
Most if not all method calls can fail and so every GO Method returns an error. This must somehow be ported to the C world.
For the C interop i created a Result struct containing result and potential error, wrap the GO results in it and return a handle to the result struct.
With methods ResultGetErrorCode(handleToResultStruct) ResultGetErrorMessage(handleToResultStruct), ResultGetValue(handleToResultStruct) it is possible to retrieve the error message, error code and the Value.
I do have the feeling that this approach is not very C-Stylish, where as far as i know, its more common to return an error code, and use output paramaters for results. Thats something on the other side i dont know how to implement using CGO.
Below you find some code to show how i realized it so far.
Functionality in GO
type Device struct { } func NewDevice(param1 int, param2 string) Device { //... } func (device *Device) ReadSomething() (string, error) { //... } CGO interop layer
Here a C-Interface is defined, converting all GO Types, etc to C compatible types
The idea is to wrap the result and the error in a go struct, that can be turned into a handle and passed to the C or whatever interop world.
func NewDevice(param1 int, param2 *C.char) C.ulonglong { device := NewDevice(param1, C.GoString(param2)) return C.ulonglong(cgo.NewHandle(device)) } func DeviceReadSomething(handle C.ulonglong) C.ulonglong { device := cgo.Handle(handle).Value().(Device) value, err := device.ReadSomething() result = Result{ ErrorCode: ... ErrorMessage: err.Error() Value: value } return C.ulonglong(cgo.NewHandle(result)) } // error struct type Result Struct { ErrorCode int ErrorMessage string Value string } func ResultGetErrorCode(C.ulonglong handle) int { result := cgo.Handle(handle).Value().(Result) return result.ErrorCode } func ResultGetErrorMessage(C.ulonglong handle) *C.char { //... } func ResultGetValue(C.ulonglong handle) *C.char { //... } .NET Wrapper
How a .NET wrapper for that device could look like, and how the interio works
public class Device { public IntPtr Handle { get; } public Device(int param1, string param2) { this.Handle = Interop.NewDevice(param1, param2) } private static class Interop { [DllImport(Lib.Path)] public static extern IntPtr NewDevice(int param1, string param2); // ... } public string Something { get { var result = new Result(Interop.ReadSomething(this.Handle)); if(result.Exception is not null) throw result.Exception; return result.Value; } } } public class Result { public string Value { get; } public Exception Exception { get; } public Result(IntPtr handle) { var errorCode = Interop.ResultGetErrorCode(handle); var errorMessage = Interop.ResultGetErrorMessage(handle); this.Value = Marshal.PtrToStringAnsi(Interop.ResultGetValue(handle)) if(errorCode != 0) { this.Exception = DeviceException.Create(errorCode, errorMessage); } } private static class Interop { [DllImport(Lib.Path)] public int ResultGetErrorCode(IntPtr handle); // ... } }