Skip to content
Draft
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
35 changes: 35 additions & 0 deletions .github/workflows/native-host-calls.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ jobs:
tests/native_host_call_host.c -ldl -o dist/native_host_call_host
./dist/native_host_call_host ./dist/libportapy.so | tee dist/host-call-output.txt
grep -qx 'native-host-calls: ok' dist/host-call-output.txt
- name: Execute managed environment ABI from C
run: |
cc -std=c11 -Wall -Wextra -Werror -Iinclude \
tests/native_managed_environment_host.c -ldl \
-o dist/native_managed_environment_host
./dist/native_managed_environment_host ./dist/libportapy.so \
| tee dist/managed-environment-output.txt
grep -qx 'managed-environment: ok' dist/managed-environment-output.txt
- name: Execute public tuple ABI from C
run: |
cc -std=c11 -Wall -Wextra -Werror -Iinclude \
Expand Down Expand Up @@ -82,6 +90,33 @@ jobs:
$output = & dist/native_host_call_host.exe dist/portapy.dll
$output | Out-File -Encoding utf8 dist/host-call-output.txt
if ($output -ne 'native-host-calls: ok') { throw "unexpected host-call output: $output" }
- name: Execute managed environment ABI from C
shell: powershell
run: |
gcc -std=c11 -Wall -Wextra -Werror -Iinclude `
tests/native_managed_environment_host.c `
-o dist/native_managed_environment_host.exe
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$output = & dist/native_managed_environment_host.exe dist/portapy.dll
$output | Out-File -Encoding utf8 dist/managed-environment-output.txt
if ($output -ne 'managed-environment: ok') { throw "unexpected managed environment output: $output" }
- name: Execute direct C# environment API
shell: powershell
run: |
$project = 'tests/csharp/PortaPy.ManagedSmoke/PortaPy.ManagedSmoke.csproj'
dotnet build $project -c Release *>&1 `
| Tee-Object -FilePath dist/csharp-managed-build.log
$buildExit = $LASTEXITCODE
if ($buildExit -ne 0) { exit $buildExit }
$outputDirectory = 'tests/csharp/PortaPy.ManagedSmoke/bin/Release/net8.0'
Copy-Item dist/portapy.dll "$outputDirectory/portapy.dll"
& dotnet "$outputDirectory/PortaPy.ManagedSmoke.dll" *>&1 `
| Tee-Object -FilePath dist/csharp-managed-run.log
$runExit = $LASTEXITCODE
if ($runExit -ne 0) { exit $runExit }
$output = Get-Content dist/csharp-managed-run.log
$output | Out-File -Encoding utf8 dist/csharp-managed-environment-output.txt
if ($output[-1] -ne 'csharp-managed-environment: ok') { throw "unexpected C# output: $output" }
- name: Execute public tuple ABI from C
shell: powershell
run: |
Expand Down
229 changes: 229 additions & 0 deletions bindings/csharp/PortaPy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
using System;
using System.Collections.Generic;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;

namespace PortaPy;

public enum Status : int
{
Ok = 0,
InvalidArgument = 1,
CompileError = 2,
RuntimeError = 3,
TypeError = 4,
NotFound = 5,
Closed = 6,
InvalidHandle = 7,
Interrupted = 8,
AbiMismatch = 9,
}

public sealed class PortaPyException : Exception
{
public Status Status { get; }

public PortaPyException(Status status)
: base($"PortaPy failed with status {status} ({(int)status}).")
{
Status = status;
}
}

public sealed class Environment : IDisposable
{
private const string LibraryName = "portapy";

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate Status NativeCallback(
IntPtr context,
ulong runtime,
IntPtr arguments,
nuint argumentCount,
out ulong result
);

[DllImport(
LibraryName,
CallingConvention = CallingConvention.Cdecl,
ExactSpelling = true
)]
private static extern Status portapy_environment_create(out IntPtr environment);

[DllImport(
LibraryName,
CallingConvention = CallingConvention.Cdecl,
ExactSpelling = true
)]
private static extern Status portapy_environment_destroy(IntPtr environment);

[DllImport(
LibraryName,
CallingConvention = CallingConvention.Cdecl,
CharSet = CharSet.Unicode,
ExactSpelling = true
)]
private static extern Status portapy_environment_add_callback_utf16(
IntPtr environment,
[MarshalAs(UnmanagedType.LPWStr)] string name,
nuint nameLength,
NativeCallback callback,
IntPtr context
);

[DllImport(
LibraryName,
CallingConvention = CallingConvention.Cdecl,
CharSet = CharSet.Unicode,
ExactSpelling = true
)]
private static extern Status portapy_environment_execute_utf16(
IntPtr environment,
[MarshalAs(UnmanagedType.LPWStr)] string source,
nuint sourceLength
);

[DllImport(
LibraryName,
CallingConvention = CallingConvention.Cdecl,
ExactSpelling = true
)]
private static extern Status portapy_value_from_none(
ulong runtime,
out ulong value
);

private readonly List<NativeCallback> callbackRoots = new();
private readonly object callbackLock = new();
private IntPtr handle;
private ExceptionDispatchInfo? pendingManagedException;
private bool disposed;

public Environment()
{
ThrowIfError(portapy_environment_create(out handle));
if (handle == IntPtr.Zero)
{
throw new PortaPyException(Status.RuntimeError);
}
}

~Environment()
{
Dispose(false);
}

public void Add(Action callback)
{
ArgumentNullException.ThrowIfNull(callback);
Add(callback.Method.Name, callback);
}

public void Add(string name, Action callback)
{
ObjectDisposedException.ThrowIf(disposed, this);
ArgumentException.ThrowIfNullOrWhiteSpace(name);
ArgumentNullException.ThrowIfNull(callback);

NativeCallback nativeCallback = (
IntPtr context,
ulong runtime,
IntPtr arguments,
nuint argumentCount,
out ulong result
) =>
{
result = 0;
if (argumentCount != 0)
{
return Status.InvalidArgument;
}

try
{
callback();
return portapy_value_from_none(runtime, out result);
}
catch (Exception error)
{
lock (callbackLock)
{
pendingManagedException = ExceptionDispatchInfo.Capture(error);
}
return Status.RuntimeError;
}
};

ThrowIfError(
portapy_environment_add_callback_utf16(
handle,
name,
(nuint)name.Length,
nativeCallback,
IntPtr.Zero
)
);

callbackRoots.Add(nativeCallback);
}

public void Execute(string source)
{
ObjectDisposedException.ThrowIf(disposed, this);
ArgumentNullException.ThrowIfNull(source);

Status status = portapy_environment_execute_utf16(
handle,
source,
(nuint)source.Length
);

ExceptionDispatchInfo? callbackError = null;
lock (callbackLock)
{
if (pendingManagedException is not null)
{
callbackError = pendingManagedException;
pendingManagedException = null;
}
}
callbackError?.Throw();
ThrowIfError(status);
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

private void Dispose(bool disposing)
{
if (disposed)
{
return;
}
disposed = true;

IntPtr current = handle;
handle = IntPtr.Zero;
if (current != IntPtr.Zero)
{
Status status = portapy_environment_destroy(current);
if (disposing)
{
ThrowIfError(status);
}
}

callbackRoots.Clear();
}

private static void ThrowIfError(Status status)
{
if (status != Status.Ok)
{
throw new PortaPyException(status);
}
}
}
38 changes: 38 additions & 0 deletions docs/CSHARP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Direct C# hosting

`portapy.dll` exposes a managed-host ABI directly. No custom C/C++ shim is
required. Include `bindings/csharp/PortaPy.cs` in the C# project and place
`portapy.dll` beside the built application.

```csharp
using PortaPy;

void helloWorld()
{
Console.WriteLine("Hello, world!");
}

using var env = new PortaPy.Environment();
env.Add(helloWorld);
env.Execute("helloWorld()\n");
```

`Environment.Add(Action)` uses the managed method name as the Python global
name. An explicit name is also supported:

```csharp
env.Add("hello_world", helloWorld);
env.Execute("hello_world()\n");
```

The environment owns the native runtime and callback registry. The managed
wrapper keeps callback delegates alive until disposal and rethrows exceptions
raised by managed callbacks after native execution returns.

The underlying public exports are:

- `portapy_environment_create`
- `portapy_environment_destroy`
- `portapy_environment_get_runtime`
- `portapy_environment_add_callback_utf16`
- `portapy_environment_execute_utf16`
46 changes: 46 additions & 0 deletions include/portapy_managed.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#ifndef PORTAPY_MANAGED_H
#define PORTAPY_MANAGED_H

#include "portapy.h"

#ifdef __cplusplus
extern "C" {
#endif

typedef struct portapy_environment portapy_environment;

typedef portapy_status (PORTAPY_CALL *portapy_environment_callback)(
void *context,
portapy_runtime runtime,
const portapy_value *arguments,
size_t argument_count,
portapy_value *out_result
);

PORTAPY_API portapy_status PORTAPY_CALL portapy_environment_create(
portapy_environment **out_environment
);
PORTAPY_API portapy_status PORTAPY_CALL portapy_environment_destroy(
portapy_environment *environment
);
PORTAPY_API portapy_runtime PORTAPY_CALL portapy_environment_get_runtime(
const portapy_environment *environment
);
PORTAPY_API portapy_status PORTAPY_CALL portapy_environment_add_callback_utf16(
portapy_environment *environment,
const uint16_t *name,
size_t name_length,
portapy_environment_callback callback,
void *context
);
PORTAPY_API portapy_status PORTAPY_CALL portapy_environment_execute_utf16(
portapy_environment *environment,
const uint16_t *source,
size_t source_length
);

#ifdef __cplusplus
}
#endif

#endif
3 changes: 3 additions & 0 deletions native/environment_glue.c
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,6 @@ portapy_status PORTAPY_CALL portapy_global_name_copy_utf8(
}
return PORTAPY_OK;
}

#include "portapy_managed.h"
#include "managed_environment_glue.c"
Loading
Loading