-1

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);
        
        // ...
    }
}
  • "I am not the greatest C programmer" - so address that. Write your common denominator library in the common denominator language. All this yak shaving with go is just you procrastinating. – whatsisname Apr 16 '22 at 02:32

1 Answers1

0

I have no idea about CGO, so I'm only going to comment on the .Net parts of the question.

  1. For things that are potentially 'complex', I would recommend using a method rather than a property. I.e. public string GetSomething().... That better communicates that the operation might take time, might fail etc.
  2. Please provide appropriate parameter names and comments for your APIs, including things like exception types methods may throw. When building the project you should turn on to output a xml documentation file so the users have an easy way to access any comments.
  3. You might consider writing the interface in c++/cli instead of using PInvoke. That might make it easier to handle more complex data-types, at the cost of using a somewhat uncommon language.
  4. You might consider using some generic result datatype instead of exceptions. See Result object vs throwing exception for the full discussion.
  5. I do not think you would need individual calls to get the error code, message & result. It should be possible to Marshall a struct, but I have mostly used c++/cli, so I'm not very familiar with the marshaller.
  6. Consider what versions of .Net you want to support. There is generational shift from the older .Net framework to .Net core/5/6. You can target .Net standard to get the widest compatibility, but it may limit the types you can use in your API.
JonasH
  • 3,426
  • 16
  • 13