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

UTF-8 string literals #167

Merged
merged 8 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
dotnet-version: '9.x'

- name: ".NET pack"
run: dotnet pack "./src/cs" --nologo --verbosity minimal --configuration Release -p:PackageVersion="${{ steps.set-version.outputs.VERSION }}" -p:RepositoryBranch="${{ github.head_ref || github.ref_name }}" -p:RepositoryCommit="${{ github.sha }}"
run: dotnet pack "./src/cs" --nologo --verbosity minimal --configuration Release /property:Version="${{ steps.set-version.outputs.VERSION }}" -p:PackageVersion="${{ steps.set-version.outputs.VERSION }}" -p:RepositoryBranch="${{ github.head_ref || github.ref_name }}" -p:RepositoryCommit="${{ github.sha }}"

- name: "Upload packages to MyGet"
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pre-release == 'true'
Expand Down
32 changes: 18 additions & 14 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
- [How to use `C2CS`](#how-to-use-c2cs)
- [Installing and using `c2ffi`](#installing-and-using-c2ffi)
- [Execute `c2cs`](#execute-c2cs)
- [How to use `C2CS.Runtime`](#how-to-use-c2csruntime)
- [How to use the `Interop.Runtime`](#how-to-use-the-interopruntime)
- [Building `C2CS` from source](#building-c2cs-from-source)
- [Prerequisites](#prerequisites)
- [Visual Studio / Rider / MonoDevelop](#visual-studio--rider--monodevelop)
- [Visual Studio / Rider](#visual-studio--rider)
- [Command Line Interface (CLI)](#command-line-interface-cli)
- [Debugging `C2CS` from source](#debugging-c2cs-from-source)
- [Debugging using logging](#debugging-using-logging)
Expand All @@ -29,7 +29,7 @@ See [LESSONS-LEARNED.md](./LESSONS-LEARNED.md).

## Installing `C2CS`

`C2CS` is distributed as a NuGet tool. To get started, the .NET 8 software development kit (SDK) is required.
`C2CS` is distributed as a NuGet tool. To get started, the .NET 9 software development kit (SDK) is required.

### Latest release of `C2CS`

Expand All @@ -51,31 +51,35 @@ dotnet nuget locals all --clear

## How to use `C2CS`

To generate C# bindings for a C library you need to install and use the `c2ffi` tool and setup a couple configuration files. See the [`helloworld`](../src/cs/examples/helloworld/) example projects for an example.
To generate C# bindings for a C library you need to first install and use the `c2ffi` tool. Then setup a couple configuration files. See the [`helloworld-bindgen`](../src/cs/examples/helloworld/helloworld-bindgen) example projects for an example of these configuration files.

### Installing and using `c2ffi`

See the auxiliary project `Getting Started` section: https://github.com/bottlenoselabs/c2ffi#getting-started.

You should extract all the platform specific FFIs you wish to have as target platforms. See the [`helloworld-compile-c-library-and-generate-bindings`](../src/cs/examples/helloworld/helloworld-compile-c-library-and-generate-bindings/) for example configuration files for Windows, macOS, and Linux platforms.
You should extract all the platform specific FFIs you wish to have as target platforms using `c2ffi extract --config ...`. See the [`helloworld-bindgen/config-extract.json`](../src/cs/examples/helloworld/helloworld-bindgen/config-extract.json) for example configuration file for Windows, macOS, and Linux platforms.

Once all the platform FFIs are extracted to a directory, merge them together into a cross-platform FFI using `c2ffi merge` option.
Once all the platform FFIs are extracted to a directory, merge them together into a cross-platform FFI using `c2ffi merge --inputDirectoryPath ... --outputFilePath ...` option.

See the [`helloworld-bindgen`]([`helloworld-bindgen`](../src/cs/examples/helloworld/helloworld-bindgen/Program.cs).) C# program for example of using `c2ffi` from command line.

Once you have a cross-platform FFI `.json` file, you are ready to use `c2cs`.

### Execute `c2cs`

Run `c2cs --config` from terminal specifying a configuration file. See the [`config-generate-cs.json`](/src/cs/examples/helloworld/helloworld-compile-c-library-and-generate-bindings/config-generate-cs.json) for an example in the `helloworld-compile-c-library-and-generate-bindings` example.
Run `c2cs --config ...` from terminal specifying a configuration file. See the [`config-generate-cs.json`](../src/cs/examples/helloworld/helloworld-bindgen/config-generate-cs.json) for an example configuration `.json` file.

## How to use the `Interop.Runtime`

## How to use `C2CS.Runtime`
The [`Interop.Runtime`](../src/cs/examples/helloworld/helloworld-app/Generated/Runtime.g.cs) C# code is by default generated to a new file as `Runtime.g.cs`. The `Interop.Runtime` namespace contains helper structs, methods, and other kind of "glue" that make interoperability with C in C# easier and more idiomatic.

The `C2CS.Runtime` C# code is directly added to the bottom of the generated bindings in a class named `Runtime` with a C# region named `C2CS.Runtime`. The `Runtime` static class contains helper structs, methods, and other kind of "glue" that make interoperability with C in C# easier and more idiomatic.
See the [HelloWorld example](src/cs/examples/helloworld/helloworld-app/Program.cs) for C# code that uses and explains how to use `Interop.Runtime`.

## Building `C2CS` from source

### Prerequisites

1. Install [.NET 8 SDK](https://dotnet.microsoft.com/download).
1. Install [.NET 9 SDK](https://dotnet.microsoft.com/download).
2. Install build tools for C/C++.
- Windows:
1. Install Git Bash. (Usually installed with Git for Windows: https://git-scm.com/downloads.)
Expand All @@ -87,9 +91,9 @@ The `C2CS.Runtime` C# code is directly added to the bottom of the generated bind
4. Install CMake: ```brew install cmake```
- Linux:
1. Install the software build tools for your distro including GCC, Clang, and CMake.
3. Clone the repository with submodules: `git clone --recurse-submodules https://github.com/lithiumtoast/c2cs.git`.
3. Clone the repository with submodules: `git clone --recurse-submodules https://github.com/bottlenoselabs/c2cs.git`.

### Visual Studio / Rider / MonoDevelop
### Visual Studio / Rider

Open `./C2CS.sln`

Expand Down Expand Up @@ -131,5 +135,5 @@ Here you will find examples of C libraries being demonstrated with `C2CS` as smo

Hello world example of callings C functions from C#. This is meant to be minimalistic to demonstrate the minimum required things to get this working.

1. Run the C# project [`helloworld-compile-c-library-and-generate-bindings`](../src/cs/examples/helloworld/helloworld-compile-c-library-and-generate-bindings/Program.cs). This builds the example shared library and generate the bindings for the [`my_c_library`](../src/cs/examples/helloworld/helloworld-compile-c-library-and-generate-bindings/my_c_library/) C project. The C# bindings will be written to [`my_c_library.cs`](../src/cs/examples/helloworld/helloworld-app/Generated/my_c_library.gen.cs).
2. Run the C# project [`helloworld-cs`](../src/cs/examples/helloworld/helloworld-app/Program.cs). You should see output to the console of C functions being called from C#.
1. Run the C# project [`helloworld-bindgen`](../src/cs/examples/helloworld/helloworld-bindgen/Program.cs). This builds the example shared C library and generate the bindings for the [`my_c_library`](../src/cs/examples/helloworld/helloworld-bindgen/my_c_library) C project. The C# bindings will be written to [`my_c_library.g.cs`](../src/cs/examples/helloworld/helloworld-app/Generated/my_c_library.g.cs).
2. Run the C# project [`helloworld-app`](../src/cs/examples/helloworld/helloworld-app/Program.cs). You should see output to the console of C functions being called from C#.
9 changes: 8 additions & 1 deletion src/cs/c2cs.sln
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "helloworld-bindgen", "examp
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "c2cs.Tool", "production\c2cs.Tool\c2cs.Tool.csproj", "{1B67F4A2-24E6-43A6-A031-918DD44CD991}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "c2cs.Interop.Runtime", "production\c2cs.Interop.Runtime\c2cs.Interop.Runtime.csproj", "{DB492E65-25D9-44A0-86F4-ABB07283BEE7}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Interop.Runtime", "production\Interop.Runtime\Interop.Runtime.csproj", "{DB492E65-25D9-44A0-86F4-ABB07283BEE7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "helloworld-app", "examples\helloworld\helloworld-app\helloworld-app.csproj", "{EA3E3362-32A5-4395-BF56-39BDBE2B7CC9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "c2cs.Tests.Common", "tests\c2cs.Tests.Common\c2cs.Tests.Common.csproj", "{1AC16D87-0DB7-42AD-8CB9-4738D1B2D8D4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -44,6 +46,10 @@ Global
{EA3E3362-32A5-4395-BF56-39BDBE2B7CC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EA3E3362-32A5-4395-BF56-39BDBE2B7CC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EA3E3362-32A5-4395-BF56-39BDBE2B7CC9}.Release|Any CPU.Build.0 = Release|Any CPU
{1AC16D87-0DB7-42AD-8CB9-4738D1B2D8D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1AC16D87-0DB7-42AD-8CB9-4738D1B2D8D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1AC16D87-0DB7-42AD-8CB9-4738D1B2D8D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1AC16D87-0DB7-42AD-8CB9-4738D1B2D8D4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{1BE51A85-A187-46CC-A307-04A66316CD55} = {65B41709-3C9F-4556-83C8-A4380AC14E56}
Expand All @@ -52,5 +58,6 @@ Global
{1B67F4A2-24E6-43A6-A031-918DD44CD991} = {D5FE4F37-21B4-498E-B6B7-CB9DDBE5937A}
{DB492E65-25D9-44A0-86F4-ABB07283BEE7} = {D5FE4F37-21B4-498E-B6B7-CB9DDBE5937A}
{EA3E3362-32A5-4395-BF56-39BDBE2B7CC9} = {24028D61-CCE1-4893-9BD6-1D7C28563DC8}
{1AC16D87-0DB7-42AD-8CB9-4738D1B2D8D4} = {65B41709-3C9F-4556-83C8-A4380AC14E56}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// <auto-generated>
// This code was generated by the following tool on 2025-01-01 22:44:49 GMT-05:00:
// https://github.com/bottlenoselabs/c2cs (v2025-01-01 22:44:49 GMT-05:00)
// This code was generated by the following tool on 2025-01-03 14:27:37 GMT-05:00:
// https://github.com/bottlenoselabs/c2cs (v2025-01-03 14:27:37 GMT-05:00)
//
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
// </auto-generated>
Expand Down
60 changes: 57 additions & 3 deletions src/cs/examples/helloworld/helloworld-app/Generated/Runtime.g.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// <auto-generated>
// This code was generated by the following tool on 2025-01-01 22:44:49 GMT-05:00:
// This code was generated by the following tool on 2025-01-03 14:27:37 GMT-05:00:
// https://github.com/bottlenoselabs/c2cs (v0.0.0.0)
//
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
Expand All @@ -22,6 +22,9 @@ namespace Interop.Runtime;
[StructLayout(LayoutKind.Sequential)]
public readonly struct CBool : IEquatable<CBool>
{
/// <summary>
/// The value.
/// </summary>
public readonly byte Value;

private CBool(bool value)
Expand Down Expand Up @@ -134,6 +137,9 @@ public static bool Equals(CBool left, CBool right)
[StructLayout(LayoutKind.Sequential)]
public readonly struct CChar : IEquatable<byte>, IEquatable<CChar>
{
/// <summary>
/// The value.
/// </summary>
public readonly byte Value;

private CChar(byte value)
Expand Down Expand Up @@ -251,13 +257,32 @@ public static bool Equals(CChar left, CChar right)
[StructLayout(LayoutKind.Sequential)]
public readonly unsafe struct CString : IEquatable<CString>, IDisposable
{
/// <summary>
/// The pointer.
/// </summary>
public readonly IntPtr Pointer;

/// <summary>
/// Gets a value indicating whether this <see cref="CString" /> is a null pointer.
/// </summary>
public bool IsNull => Pointer == IntPtr.Zero;

#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER
/// <summary>
/// Initializes a new instance of the <see cref="CString" /> struct.
/// </summary>
/// <param name="value">The span.</param>
public CString(ReadOnlySpan<byte> value)
{
#pragma warning disable CS8500
fixed (byte* pointer = value)
{
Pointer = (IntPtr)pointer;
}
#pragma warning restore CS8500
}
#endif

/// <summary>
/// Initializes a new instance of the <see cref="CString" /> struct.
/// </summary>
Expand Down Expand Up @@ -306,7 +331,7 @@ public static explicit operator CString(IntPtr value)
}

/// <summary>
/// Performs an explicit conversion from an <see cref="IntPtr" /> to a <see cref="CString" />.
/// Performs a conversion from an <see cref="IntPtr" /> to a <see cref="CString" />.
/// </summary>
/// <param name="value">The pointer value.</param>
/// <returns>
Expand Down Expand Up @@ -341,6 +366,32 @@ public static CString From(byte* value)
return new CString((IntPtr)value);
}

#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER
/// <summary>
/// Performs an explicit conversion from a <see cref="ReadOnlySpan{T}" /> to a <see cref="CString" />.
/// </summary>
/// <param name="value">The pointer.</param>
/// <returns>
/// The resulting <see cref="IntPtr" />.
/// </returns>
public static explicit operator CString(ReadOnlySpan<byte> value)
{
return new CString(value);
}

/// <summary>
/// Performs a conversion from a <see cref="ReadOnlySpan{T}" /> to a <see cref="CString" />.
/// </summary>
/// <param name="value">The pointer value.</param>
/// <returns>
/// The resulting <see cref="CString" />.
/// </returns>
public static CString FromReadOnlySpan(ReadOnlySpan<byte> value)
{
return new CString(value);
}
#endif

/// <summary>
/// Performs an implicit conversion from a <see cref="CString" /> to a <see cref="IntPtr" />.
/// </summary>
Expand All @@ -354,7 +405,7 @@ public static implicit operator IntPtr(CString value)
}

/// <summary>
/// Performs an implicit conversion from a <see cref="CString" /> to a <see cref="IntPtr" />.
/// Performs a conversion from a <see cref="CString" /> to a <see cref="IntPtr" />.
/// </summary>
/// <param name="value">The pointer.</param>
/// <returns>
Expand Down Expand Up @@ -531,6 +582,9 @@ public static unsafe class CStrings
[StructLayout(LayoutKind.Sequential)]
public readonly unsafe struct CStringWide : IEquatable<CStringWide>
{
/// <summary>
/// The pointer.
/// </summary>
public readonly IntPtr Pointer;

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// <auto-generated>
// This code was generated by the following tool on 2025-01-01 22:44:49 GMT-05:00:
// This code was generated by the following tool on 2025-01-03 14:27:37 GMT-05:00:
// https://github.com/bottlenoselabs/c2cs (v0.0.0.0)
//
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
Expand Down Expand Up @@ -49,6 +49,8 @@ public static unsafe partial class my_c_library
[UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })]
public static partial void hw_pass_string(CString s);

public static readonly CString HW_STRING_POINTER = (CString)"Hello world using UTF-8 string literal from the C library's data segment!"u8;

public enum hw_my_enum_week_day : int
{
HW_MY_ENUM_WEEK_DAY_UNKNOWN = 0,
Expand Down
36 changes: 30 additions & 6 deletions src/cs/examples/helloworld/helloworld-app/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information.

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Interop.Runtime;
using static helloworld.my_c_library;
Expand All @@ -12,9 +13,27 @@ private static unsafe void Main()
{
hw_hello_world();

var cString1 = (CString)"Hello world from C#!";
#if NET7_0_OR_GREATER
// NOTE: If you apply the `u8`, it's a UTF-8 string literal and does not allocate the string on the heap!
// Only available in C# 11 (.NET 7+). See https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/reference-types#utf-8-string-literals
var cString1 = (CString)"Hello world from C# using UTF-8 string literal! No need to free this string!"u8;
hw_pass_string(cString1);
Marshal.FreeHGlobal(cString1);

// NOTE: This is particularly useful if you have C defines to strings which are stored in the data segment of the loaded C library.
hw_pass_string(HW_STRING_POINTER);
#endif

// NOTE: If you don't apply the `u8` it's a UTF-16 string which needs to be converted to UTF-8 and allocated.
// This is done by calling `CString.FromString` or using the explicit CString conversion operator.
// You additionally need to call `Marshal.FreeHGlobal()` when you are done with it or you have a memory leak!
var cString2 = CString.FromString("Hello world from C# using UTF-16 converted UTF-8 and allocated! Don't forgot to free this string!");
hw_pass_string(cString2);
Marshal.FreeHGlobal(cString2);

// NOTE: You can also use `using` syntax so you don't forgot to call `Marshal.FreeHGlobal()` at the scope end.
// Just don't use `using` syntax when using UTF-8 string literals or your app will crash!
using var cString3 = (CString)"Hello world again from C# using UTF-16 converted UTF-8 and allocated! Don't forgot to free this string!";
hw_pass_string(cString3);

hw_pass_integers_by_value(65449, -255, 24242);

Expand All @@ -24,22 +43,27 @@ private static unsafe void Main()
hw_pass_integers_by_reference(&a, &b, &c);

#if NET5_0_OR_GREATER
// NOTE: Function pointers provide a more efficient way to execute callbacks from C instead of using delegates.
// A struct will be generated to "house" the function pointer regardless, in this case: `FnPtr_CString_Void`.
// - It uses the same naming as `System.Func<>`. The last type on the name is always the return type. In this case 'void`.
// Only available in C# 9 (.NET 5+). See https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/unsafe-code#function-pointers
// Additionally function pointers need to use the `address-of` operator (&) to a C# static function marked with the UnmanagedCallersOnly attribute. See https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute?view=net-9.0
var functionPointer = new FnPtr_CString_Void(&Callback);
#else
var functionPointer = new FnPtr_CString_Void(Callback);
#endif
var cString2 = (CString)"Hello from callback!";
hw_invoke_callback(functionPointer, cString2);
Marshal.FreeHGlobal(cString2);

using var cStringCallback = (CString)"Hello from callback!";
hw_invoke_callback(functionPointer, cStringCallback);
}

#if NET5_0_OR_GREATER
// NOTE: Function pointers need to use the UnmanagedCallersOnly attribute. See https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute?view=net-9.0
[UnmanagedCallersOnly]
#endif
private static void Callback(CString param)
{
// This C# function is called from C

// Get the string and print it
var str = param.ToString();
Console.WriteLine(str);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
"outputFileDirectory": "./../helloworld-app/Generated",
"className": "my_c_library",
"namespaceName": "helloworld",
"targetFrameworkMoniker": "net8.0"
"targetFrameworkMoniker": "net9.0"
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
#define MY_C_LIBRARY_API_DECL extern __attribute__ ((visibility("default")))
#endif

#define HW_STRING_POINTER "Hello world using UTF-8 string literal from the C library's data segment!"

typedef enum hw_my_enum_week_day {
HW_MY_ENUM_WEEK_DAY_UNKNOWN,
HW_MY_ENUM_WEEK_DAY_MONDAY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ namespace Interop.Runtime;
[StructLayout(LayoutKind.Sequential)]
public readonly struct CBool : IEquatable<CBool>
{
/// <summary>
/// The value.
/// </summary>
public readonly byte Value;

private CBool(bool value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ namespace Interop.Runtime;
[StructLayout(LayoutKind.Sequential)]
public readonly struct CChar : IEquatable<byte>, IEquatable<CChar>
{
/// <summary>
/// The value.
/// </summary>
public readonly byte Value;

private CChar(byte value)
Expand Down
Loading
Loading