samples icon indicating copy to clipboard operation
samples copied to clipboard

NativeLibrary example is missing mention of F#

Open lukemerrick opened this issue 2 years ago • 4 comments

Issue description

The AOT Native Library example does not include any F# code or documentation. Attempting to adapt the example to F# as in the below example causes a compiler error This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect. but I cannot find any mention of this limitation, workarounds, future support date, etc.

This is particularly confusing/frustrating because there is this lovely 2+ year old example of doing an F# native library in .NET SDK version 3, but there seems to be little else on the web discussing F# native libraries in more modern .NET.

It would be superb if someone would be willing to add some or all of the following information to either to this example repo or the official docs or both!

Information which would be super helpful to me and others like me

  • Whether there is an official mainline way to compile a Native Library from F# in the same way as C# in .NET 7
  • The backstory to the compiler error and the current outlook for adding UnmanagedCallersOnly support to the F# compiler
  • Best practices for working around the limitations in the current release of .NET 7 (is it best to use CoreRT despite it being superseded by mainline .NET?)

Thank you for reading!

// My attempt to expose a simple function in a Native Library build.
namespace HelloLib

open System.Runtime.InteropServices

module Say =
    [<UnmanagedCallersOnly(EntryPoint= "hello")>]
    let hello name =
        printfn "Hello %s" name

Target framework

Check the .NET target framework(s) being used, and include the version number(s).

  • [ ] .NET Core
  • [ ] .NET Framework
  • [X] .NET Standard 7.0.102

If using the .NET Core SDK, include dotnet --info output. If using .NET Framework without the .NET Core SDK, include info from Visual Studio's Help > About Microsoft Visual Studio dialog.

dotnet --info output or About VS info
dotnet --info
.NET SDK:
 Version:   7.0.102
 Commit:    4bbdd14480

Runtime Environment:
 OS Name:     manjaro
 OS Version:  
 OS Platform: Linux
 RID:         manjaro-x64
 Base Path:   /usr/share/dotnet/sdk/7.0.102/

Host:
  Version:      7.0.2
  Architecture: x64
  Commit:       d037e070eb

.NET SDKs installed:
  7.0.102 [/usr/share/dotnet/sdk]

.NET runtimes installed:
  Microsoft.NETCore.App 7.0.2 [/usr/share/dotnet/shared/Microsoft.NETCore.App]

Other architectures found:
  None

Environment variables:
  Not set

global.json file:
  Not found

Learn more:
  https://aka.ms/dotnet/info

Download .NET:
  https://aka.ms/dotnet/download

lukemerrick avatar Jan 31 '23 03:01 lukemerrick

Just want to mention for any readers that I did get this working with F# on Mac OS M3.

// native_fsharp.fsproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
    <PublishAot>true</PublishAot>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Library.fs" />
  </ItemGroup>

</Project>
// Library.fs
module NativeFSharp

open System.Runtime.InteropServices
open System.Runtime.CompilerServices
open System

[<UnmanagedCallersOnly(EntryPoint = "Add", CallConvs = [| typeof<CallConvCdecl> |])>]
let Add (a: int) (b: int) : int =
    // printfn uses reflection, won't work properly with AOT
    Console.WriteLine "Printing from F#!"
    a + b
// main.c
#include <stdio.h>
#include <dlfcn.h>

typedef int (*AddFunc)(int, int);

int main()
{
    void *handle = dlopen("./bin/Release/net9.0/osx-arm64/publish/native_fsharp.dylib", RTLD_LAZY);
    if (!handle)
    {
        fprintf(stderr, "Error loading library: %s\n", dlerror());
        return 1;
    }

    // Get the Add function
    AddFunc add = (AddFunc)dlsym(handle, "Add");
    if (!add)
    {
        fprintf(stderr, "Error finding Add function: %s\n", dlerror());
        dlclose(handle);
        return 1;
    }

    // Call the function
    int result = add(40, 2);
    printf("Result of 40 + 2 = %d\n", result);

    dlclose(handle);
    return 0;
}

Then run the following:

dotnet publish -r osx-arm64 -c Release # Create the native lib
gcc main.c -o test_add -ldl # Build the C program
./test_add # Run it

You should see something like the this:

➜  native_fsharp ./test_add                            
Printing from F#!
Result of 40 + 2 = 42

I do get this warning from the F# compiler though:

This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect. F# Compiler[202](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/compiler-messages/fs0202)

But, it seems like it does achieve its intended effect 😄

NatElkins avatar Jan 09 '25 21:01 NatElkins

Seconded: could someone from the f# team please clarify if the compiler warning is a false positive? Are there any hidden gotchas here? I've had no problem exposing f# code as a native library with a c# wrapper around it, but I'd ideally not add a c# project just to expose my f# code without compiler warnings.

serefarikan avatar Mar 21 '25 05:03 serefarikan

@serefarikan I think there's just a set of attributes in the F# compiler that this warning appears for. I think it was probably done because no work has gone into supporting UnmanagedCallersOnly for F# per se, and I'm sure there are some scenarios where it "should" work but doesn't. If it works, it's because it's all IL under the hood and that's how UnmanagedCallersOnly works.

https://github.com/dotnet/fsharp/blob/3259edf831b702e3c10bd169143e805b03895c2b/src/Compiler/TypedTree/TcGlobals.fs#L1096

https://github.com/dotnet/fsharp/blob/3259edf831b702e3c10bd169143e805b03895c2b/src/Compiler/FSComp.txt#L42

You can probably use #nowarn to suppress this warning only in the areas you are using UnmanagedCallersOnly in F# code.

NatElkins avatar Mar 21 '25 13:03 NatElkins

Indeed. The F# compiler currently does not implement any checks needed for this to work correctly, so it would allow you to author unsupported code. If you can do those checks manually without compiler guidance, the functions can be called from native code.

T-Gro avatar Apr 08 '25 11:04 T-Gro