Possible F# Interactive PInvoke bug
Asked Answered
T

2

10

While trying to prove to a colleague that it's possible to use C++ classes from F#, I came up with the following proof of concept. The first snippet is the code he provided for the challenge, and the code snippet below is my implementation in F#.


namespace testapp {
    struct trivial_foo {
        int bar;
        __declspec(dllexport) void set(int n) { bar = n; }
        __declspec(dllexport) int get() { return bar; }
    }
}

open System.Runtime.InteropServices

type TrivialFoo =
    struct
        val bar: int
        new(_bar: int) = { bar = _bar }
    end

[<DllImport("Win32Project2.dll", EntryPoint="?get@trivial_foo@testapp@@QAEHXZ", CallingConvention = CallingConvention.ThisCall)>]
extern int trivial_foo_get(TrivialFoo& trivial_foo)

[<DllImport("Win32Project2.dll", EntryPoint="?set@trivial_foo@testapp@@QAEXH@Z", CallingConvention = CallingConvention.ThisCall)>]
extern void trivial_foo_set(TrivialFoo& trivial_foo, int bar)

type TrivialFoo with
    member this.Get() = trivial_foo_get(&this)
    member this.Set(bar) = trivial_foo_set(&this, bar)

When debugged in Visual Studio or run as a standalone program, this works predictably: TrivialFoo.Get returns the value of bar and TrivialFoo.Set assigns to it. When run from F# Interactive however, TrivialFoo.Set will not set the field. I suspect it might have something to do with accessing managed memory from unmanaged code, but that doesn't explain why it only happens when using F# Interactive. Does anyone know what's going on here?

Tarter answered 3/12, 2014 at 1:11 Comment(3)
Well, that's not the way to do interop anyway. Declare a POD struct with non+imember functions. I guess you code is failing because the struct is deemed an in param to thee methods but in any case it's all wrong. This is not how you do interop with classes.Holism
Other typical problem might be that the DLL is no in %PATH% when using FSI instead of a .exe.Collyrium
Especially since FSI shadow copies assemblies by default, so your code may be running from a temporary folder somewhere.Priestcraft
P
1

I don't think this proof of concept is a good proof of interoperability. You may be better off creating DLL export definitions from your C++ project and use the de-decorated names instead.

As a PoC: F# creates MSIL that fits in the CLI, so it can interoperate with any other CLI language out there. If that is not enough and you want native-to-net interop, consider using COM, or as mentioned above, DLL export definitions on your C++. I personally wouldn't try to interop with C++ class definitions the way you suggest here, there are way easier ways to do that.

Alternatively, just change your C++ project into a .NET C++ project and you can access the classes directly from F#, while still having the power of C++.

Naturally, you may still be wondering why the example doesn't run in FSI. You can see a hint of an answer by running the following:

> System.IO.Directory.GetCurrentDirectory();;
val it : string = "R:\TMP"

To fix this, you have a myriad of options:

  • copy Win32Project2.dll to that directory
  • add whatever path it is in to PATH
  • use an absolute path
  • use a compile-time constant
  • or use an environment variable (the path will be expanded)
  • dynamically locate the dll and dynamically bind to it (complex)

Copying is probably the easiest of these solutions.

Since FSI is meant to be a REPL, it may not be best tailored for this kind of tasks that require multiple projects, libraries or otherwise complex configurations. You may consider voting on this FSI request for support for #package to import NuGet packages, which could be used to ease such tasks.

Province answered 28/12, 2016 at 23:42 Comment(0)
A
0

The counterpart of a C++ struct in F# is not necessarily a struct. In C++, the only difference between classes and structs resides in their default access restrictions.

In F#, structs are used for value types, classes are used for reference types. One problem with value types is that they are meant to be used as immutable values, and temporary copies are often created silently.

The problem you are observing is consistent with that scenario. For some reason, F# interactive creates a copy of your struct and passes a reference to that. The C++ code then modifies the copy, leaving the original untouched.

If you switch to using a class, make sure you pin the instance before letting native code use it, or you can end up in a situation where the garbage collector moves it after the native code gets a reference to it.

Adair answered 11/7, 2017 at 14:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.