dotnet / dotnet/macios

[rfc] ExternizerAttribute: Teach generator how to do P/Invokes from interface definitions.

Open
#3,452 11 comments 3 reactions 1 assignee Claimed by @dalexsoto View on GitHub
enhancement generator iOS macOS request-for-comments
Dominant language
C#
Stars
2.9k
Forks
576
Avg merge
2d 13h
Merged PRs (30d)
96

Description

### Steps to Reproduce

Currently there is no way for the generator to produce [P/Invoke](http://www.mono-project.com/docs/advanced/pinvoke/) signatures.

### Expected Behavior

Generator must be able to produce P/Invoke definitions to avoid manual code. I must note that this is not intended to 100% remove manual code of P/Invokes but to be an additional tool to avoid it whenever possible.

### Potential Solution

Create a new attribute called `ExternizerAttribute` with the following definition:

```csharp
[AttributeUsage (AttributeTargets.Method, AllowMultiple = false)]
public class ExternizerAttribute : Attribute {

public string LibName { get; set; }
public string EntryPoint { get; set; }
public bool ExternMethodOnly { get; set; }
public Visibility ExternVisibility { get; set; } = Visibility.Disabled;
public string CustomExternMethodName { get; set; }
public bool IsFirstParamHandle { get; set; } = true;

public ExternizerAttribute (string libName) => LibName = libName;
public ExternizerAttribute (string libName, string entryPoint) : this (libName) => EntryPoint = entryPoint;
}
```

Property definitions:
* **LibName**: Native library to be opened, this will be mapped to `DllImport`'s `dllName` constructor parameter.
* **EntryPoint**: Native's library entry point to be called, this will be mapped to `DllImport`'s `EntryPoint` field.
* **ExternMethodOnly**: Instructs the generator to only create the extern signature and no wrapper method around it.
* **ExternVisibility**: Provides a way to customize extern's signature visibility, the default value is `Visibility.Disabled` which means that default visibility `private` will be used unless `ExternMethodOnly` is set to `true` this will turn visibility into `public`.
* **CustomExternMethodName**: Provides a way to customize extern method's name. By default extern method names will be suffixed with `Extern` unless `ExternMethodOnly` is set to `true` and `EntryPoint` is omitted.
* **IsFirstParamHandle**: Instructs the generator to create the P/Invoke signature with an extra parameter placed at the beginning that will be used to pass the Handle property of the container instance object.

A simple scenario of `ExternizerAttribute` would be as follows:

```csharp
[Externizer (Constants.MetalPerformanceShadersLibrary, "DoSomethingNative")]
NSSet DoSomething (MPSImage[] objs, [NullAllowed] MPSImage img, nint ratio);
```

And the output would be a wrapper method of the extern member that does casting and null checks:

```csharp
[DllImport ("/System/Library/Frameworks/MetalPerformanceShaders.framework/MetalPerformanceShaders", EntryPoint = "DoSomethingNative")]
static extern IntPtr DoSomethingExtern (IntPtr objs, IntPtr img, nint ratio);

public NSSet DoSomething (MPSImage[] objs, MPSImage img, nint ratio)
{
if (objs == null)
throw new ArgumentNullException ("objs");
var nsa_objs = NSArray.FromNSObjects (objs);

NSSet ret;
ret = Runtime.GetNSObject (DoSomethingExtern (nsa_objs.Handle, img == null ? IntPtr.Zero : img.Handle, ratio));
nsa_objs.Dispose ();

return ret;
}
```

Unfortunately `DllImport` cannot be reused because it cannot be applied to methods inside an interface definition like we do in our API definitions because the `extern` keyword is expected and some additional compiler checks. Also having our own attribute gives us the flexibility to add additional information needed to better drive the generator.

The following attributes would also be needed to be taken into account:

* `[MarshalAs (UnmanagedType)]` in return and parameters.
* `[NullAllowed]` for parameter checks.
* `[return: Release]` to allow to call `release` in the returned object if needed.
* `[Advice]`
* `[iOS, Mac]` and friends to write availability information.

### Notes

Again the scope of this attribute is not to get rid of manual code 100% but to help where possible for example it would enable the following scenarios:

* The default behaviour is meant for simple scenarios where null checks are needed before calling the unmanaged function.
* Generate the P/Invoke signature and use `[Wrap]` to do your thing.
* Expose the P/Invoke signature directly to the public API without manual code.
* C Arrays or variadic functions support is not in the scope of this attribute.

A small example where this attribute would be useful:

```c
typedef NSArray MPSImageBatch;
NSUInteger MPSImageBatchIncrementReadCount( MPSImageBatch * __nonnull batch, NSInteger amount );
```

Would only require:
```csharp
[Externizer (Constants.MetalPerformanceShadersLibrary, "MPSImageBatchIncrementReadCount")]
nuint IncrementReadCount (MPSImage [] batch, nint amount);
```

💣💥🚀

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.