F#: Calling native functions with custom type parameters

Viewed 113

I'm calling into a native DLL from F#. It's proven pretty simple for functions that take no arguments or only basic types, but I'm having a lot of trouble getting my code to work otherwise.

The C++ code is:

struct CatDef
{
    uint8_t cat;
    uint8_t arrogance;

    CatDef() : cat(0), arrogance(0) {}
};

struct HomeInfo
{
    enum { MAX_CATDEF = 5 };
    enum {
        LONE_CAT = 0x01,
    };
    CatDef  cd[MAX_CATDEF];
    uint8_t  notoriety;
    uint8_t  etc;

    HomeInfo()
        : notoriety(0), etc(0)
    {
        memset(cd, 0, sizeof(cd));
    }

    size_t howBig() const
    {
        size_t hb = 0;
        for (; hb < MAX_CATDEF; ++hb)
            if (!cd[hb].cat)
                break;
        return hb;
    }
};

struct HomeQuery
{
    char addr[ADDR_LENGTH];    // ADDR_LENGTH is 1024
    char verifiedAddr[ADDR_LENGTH];
    HomeInfo homeData;
    unsigned int id;
    Status  st;    // Status is an enum

    HomeQuery()
        : homeData()
        , id(0)
        , st(Unprocessed)
    {
        memset(addr, 0, sizeof(addr));
        memset(verifiedAddr, 0, sizeof(verifiedAddr));
    }
};

__declspec(dllexport) bool GetHomeInfo(
    HomeQuery* queries, size_t qCount, void (*callback)(HomeQuery*, size_t) = 0);

So far, I have this for F#:

[<Struct>]
type CatDef =
    [<DefaultValue>]
    val mutable cat : uint8
    [<DefaultValue>]
    val mutable arrogance : uint8

[<Struct>]
type HomeInfo =
    [<DefaultValue; MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)>]
    val mutable cd : CatDef array
    [<DefaultValue>]
    val mutable notoriety : uint8
    [<DefaultValue>]
    val mutable etc : uint8

[<Struct>]
type HomeQuery =
    [<DefaultValue; MarshalAs(UnmanagedType.LPStr, SizeConst = 1024)>]
    val mutable addr : string
    [<DefaultValue; MarshalAs(UnmanagedType.LPStr, SizeConst = 1024)>]
    val mutable verifiedAddr : string
    [<DefaultValue>]
    val mutable homeData : HomeInfo
    [<DefaultValue>]
    val mutable id : uint32
    [<DefaultValue>]
    val mutable st : int

type HomeQueryCallback = delegate of (HomeQuery array * uint) -> unit

module private Wrapper =
    [<DllImport(
        "Homes.dll",
        EntryPoint = "?GetHomeInfo@People@@NA_WBYUHomeQuery@1@IP6AX0I@Z@Z",
        CallingConvention = CallingConvention.Cdecl)>]
    extern bool GetHomeInfo(HomeQuery[] queries, uint qCount, HomeQueryCallback callback)

[<RequireQualifiedAccess>]
module HomeLib =
    let getHomeInfo queries = Wrapper.GetHomeInfo (queries, uint queries.Length, null)

For rules about type marshalling, I have been referring to https://docs.microsoft.com/en-us/dotnet/standard/native-interop/type-marshaling#default-rules-for-marshaling-common-types.

When I call HomeLib.getHomeInfo and step into the native library (Homes.dll), I see that the fields of the first (and only) item in the HomeQuery array contain garbled information. (qCount and callback are uncorrupted, and contain the expected values.) As you can see, I've tried adding MarshalAs attributes to a few fields, but this hasn't had any positive effect. Neither has decorating any of the types with [<StructLayout(LayoutKind.Sequential)>].

1 Answers

Never mind, I got it worked out.

For anyone who might care, my F# code worked with just a couple of changes:

  1. I changed UnmanagedType.LPStr to UnmanagedType.ByValTStr
  2. In the extern function declaration, I decorated HomeQuery[] queries with [<In; Out>] (i.e. [<In; Out>]HomeQuery[] queries).

That's all.

Related