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);
// ...
}
}