Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Question] - Pass struct/slice of structs from C# <---> Go #28

Open
aleqsss opened this issue Mar 17, 2023 · 2 comments
Open

[Question] - Pass struct/slice of structs from C# <---> Go #28

aleqsss opened this issue Mar 17, 2023 · 2 comments

Comments

@aleqsss
Copy link

aleqsss commented Mar 17, 2023

Hello once again @Digital-512

As mentioned in #27, I'm now seeking assistance for the task of passing a struct and also a slice of structs, from C# to a CGO exported function, and returning a struct and also a slice of structs, from a CGO exported function to the calling C# code.

Consider the following example struct in Go:

type Person struct {
	Name             string
	Hobbies          []string
	FavouriteNumbers []int
	Age              int
	Single           bool
}

Which is then called as a single struct or a slice of structs:

person := Person{
	Name:             "John",
	Hobbies:          []string{"Golf", "Hiking", "Outdoors "},
	FavouriteNumbers: []int{1, 2, 3},
	Age:              21,
	Single:           false,
}

people := []Person{
	{"John", []string{"Golf", "Hiking", "Outdoors "}, []int{1, 2, 3}, 21, false},
	{"Doe", []string{"Cycling"}, []int{4}, 32, false},
	// etc...
}

Building on top of your other provided snippets, what would be a suitable way of doing this?
How would I go on about passing both a single struct but also a slice of structs from C# to a CGO exported Go function, and also return them? Such as these examples:

type Person struct {
	Name             string
	Hobbies          []string
	FavouriteNumbers []int
	Age              int
	Single           bool
}

//export Struct
func Struct(person StructFromCSharpToGo) structFromGoToCSharp {

	// Use struct
	// return structToCSharp
}

//export StructSlice
func StructSlice(people StructSliceFromCSharpToGo) structSliceFromGoToCSharp  {

	// Use struct slice
	// return struct slice
}

func main() {}

Just get back if you need me to clearify anything, as I do see that I can get quite confusing in my examples and attempts to explain, because of my lack of knowledge in the topics.

@aleqsss aleqsss changed the title [Question] - Pass struct/object from C# <---> Go [Question] - Pass struct/slice of structs from C# <---> Go Mar 17, 2023
@Digital-512
Copy link
Contributor

Digital-512 commented Mar 17, 2023

@aleqsss The simplest way to pass an object from C# to Go and back is to serialize the object as a JSON byte array. You can use the same extension methods as for byte[].

In C#, you can use the built-in System.Text.Json namespace to serialize (convert from objects to JSON) and deserialize (convert from JSON to objects) JSON data. First you need to define a Person struct in C#:

using System.Text.Json;

public struct Person
{
    public string Name { get; set; }
    public string[] Hobbies { get; set; }
    public long[] FavouriteNumbers { get; set; }
    public long Age { get; set; }
    public bool Single { get; set; }

    public Person(string Name, string[] Hobbies, long[] FavouriteNumbers, long Age, bool Single)
    {
        this.Name = Name;
        this.Hobbies = Hobbies;
        this.FavouriteNumbers = FavouriteNumbers;
        this.Age = Age;
        this.Single = Single;
    }
}

Note that { get; set; } is added to each field. This is necessary for the serializer to work properly.

C# P/Invoke Signature:

[DllImport(libName, CallingConvention = CallingConvention.Cdecl)]
public static extern GoSlice Struct(GoSlice person);

Now create an object and serialize it. Here's an example:

// Create a new person in C#
Person person = new Person("Thomas", new string[] { "Golfing", "Horse Riding" }, new long[] { 1, 3, 7 }, 28, true);

// Serialize person object to JSON byte array
byte[] personData = JsonSerializer.SerializeToUtf8Bytes(person);

GoSlice returnedPersonSlice = Struct(personData.ToGoSlice());

// Returned structure
Person personReturned = JsonSerializer.Deserialize<Person>(returnedPersonSlice.ToByteArray());

Console.WriteLine(personReturned.Name);
Console.WriteLine(string.Join(", ", personReturned.Hobbies));
Console.WriteLine(personReturned.Age);

In the above code, we first create a new Person object with some sample data. We then use the JsonSerializer.SerializeToUtf8Bytes method to serialize the Person object to a JSON byte array.

Next, we pass the JSON byte array to the Struct function. The Struct function returns a GoSlice, which we store in the returnedPersonSlice variable.

Finally, we use the JsonSerializer.Deserialize method to deserialize the byte array from the returned GoSlice back into a Person object, which we store in the personReturned variable. We then print out the properties of the personReturned object to verify that the serialization and deserialization worked correctly.

Now, in Go code, you need to add import "encoding/json", create a Person struct and exported function Struct() as follows:

import "encoding/json"

type Person struct {
	Name             string
	Hobbies          []string
	FavouriteNumbers []int
	Age              int
	Single           bool
}

//export Struct
func Struct(person []byte) (unsafe.Pointer, int, int) {
	// Get person from C#
	var personStruct Person
	json.Unmarshal(person, &personStruct)
	fmt.Println(personStruct.Name, personStruct.Hobbies, personStruct.FavouriteNumbers, personStruct.Age, personStruct.Single)

	// Create a Person struct instance
	newPerson := Person{
		Name:             "John",
		Hobbies:          []string{"Golf", "Hiking", "Outdoors"},
		FavouriteNumbers: []int{7, 13, 42},
		Age:              21,
		Single:           false,
	}

	// Convert the newPerson object to JSON byte slice
	data, _ := json.Marshal(newPerson)
	size := len(data)

	// allocate a C buffer to hold the data
	buffer := C.CBytes(data)

	// return the C buffer, its size and capacity
	return buffer, size, size
}

In the above code, the Struct() function takes a byte slice representing a JSON object, which is then unmarshalled into a personStruct object, and the properties of this object are printed to the console.

Next, we create a new Person object newPerson in Go and serialize it to a JSON byte slice using json.Marshal. Then we allocate a C buffer to hold the serialized data, and return the buffer along with its size and capacity. As in the previous cases, don't forget to clear the unmanaged memory using C.free(buffer).

The result after executing command dotnet run should be like this:

> dotnet run
Thomas [Golfing Horse Riding] [1 3 7] 28 true
John
Golf, Hiking, Outdoors
21

If you want to pass an array of Person objects from C# to Go and back, it will be very similar to passing a single object. Here's an example for C# code:

// Create an array of people in C#
Person[] people = new Person[]{
    new Person("Thomas", new string[] { "Golfing", "Horse Riding" }, new long[] { 1, 3, 7 }, 28, true),
    new Person("James", new string[] { "Reading", "Yoga" }, new long[] { 2, 4 }, 32, true),
    new Person("Robert", new string[] { "Painting", "Photography" }, new long[] { 4, 5, 17 }, 23, false)
};

// Serialize people array to JSON byte array
byte[] peopleData = JsonSerializer.SerializeToUtf8Bytes(people);

GoSlice returnedPeopleSlice = StructSlice(peopleData.ToGoSlice());

// Returned people array
Person[] peopleReturned = JsonSerializer.Deserialize<Person[]>(returnedPeopleSlice.ToByteArray()) ?? Array.Empty<Person>();

foreach (Person person in peopleReturned)
{
    Console.WriteLine(person.Name);
    Console.WriteLine(string.Join(", ", person.Hobbies));
    Console.WriteLine(person.Age);
}

In the above code, we create an array of Person objects and serialize it to JSON byte array. Then we call function StructSlice() and deserialize returned byte slice. ?? Array.Empty<Person>() is used here because the resulting array may be empty. This ensures that peopleReturned is not null.

C# P/Invoke Signature:

[DllImport(libName, CallingConvention = CallingConvention.Cdecl)]
public static extern GoSlice StructSlice(GoSlice people);

In Go code, we create an exported function StructSlice() as follows:

//export StructSlice
func StructSlice(people []byte) (unsafe.Pointer, int, int) {
	// Get people from C#
	var peopleSlice []Person
	json.Unmarshal(people, &peopleSlice)
	for _, person := range peopleSlice {
		fmt.Println(person.Name, person.Hobbies, person.FavouriteNumbers, person.Age, person.Single)
	}

	// Create a Person struct instance
	newPeople := []Person{
		{
			Name:             "John",
			Hobbies:          []string{"Golf", "Hiking", "Outdoors"},
			FavouriteNumbers: []int{7, 13, 42},
			Age:              21,
			Single:           false,
		},
		{
			Name:             "Jennifer",
			Hobbies:          []string{"Swimming", "Outdoors"},
			FavouriteNumbers: []int{1, 9},
			Age:              30,
			Single:           true,
		},
	}

	// Convert the newPeople slice to JSON byte slice
	data, _ := json.Marshal(newPeople)
	size := len(data)

	// allocate a C buffer to hold the data
	buffer := C.CBytes(data)

	// return the C buffer, its size and capacity
	return buffer, size, size
}

As always, don't forget to call the C.free(buffer) after you have used the function.

The result after executing command dotnet run should be like this:

> dotnet run
Thomas [Golfing Horse Riding] [1 3 7] 28 true
James [Reading Yoga] [2 4] 32 true
Robert [Painting Photography] [4 5 17] 23 false
John
Golf, Hiking, Outdoors
21
Jennifer
Swimming, Outdoors
30

@aleqsss
Copy link
Author

aleqsss commented Mar 20, 2023

This is awesome! I'll comment here once I've tried it out. 👏

I'll also send you one more donation for these educating replies. I trust that the other found it's way to you?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants