2

The Csound API defines the following CS_AUDIODEVICE struct:

   typedef struct {
     char device_name[64];
     char device_id[64];
     char rt_module[64];
     int max_nchnls;
     int isOutput;
   } CS_AUDIODEVICE;

Then there is an API function called csoundGetAudioDevList:


PUBLIC int csoundGetAudioDevList(
    CSOUND* csound,
    CS_AUDIODEVICE* list,
    int isOutput
)   

that is usually called twice. The first time, a null pointer is passed in to get the number of devices. Then the second time, a pointer sized to number of devices times the size of CS_AUDIODEVICE is passed in to get the actual array of devices.

I have struggled mightily to define my DLLImport declaration and get this data marshalled correctly.

In F#, the opaque struct Csound is defined as:

[<Struct>]
type CSOUND = struct end

This works fine.


Attempt 1

[<Struct; StructLayout(LayoutKind.Sequential)>]
type CS_AUDIODEVICE =
    [<MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)>]
    val device_name: string // char[64]
    [<MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)>]
    val device_id:   string // char[64]
    [<MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)>]
    val rt_module:   string // char[64]
    val max_nchnls:  int
    val isOutput:    int

[<DllImport(csoundDLLPath, CallingConvention = CallingConvention.Cdecl)>]
extern int csoundGetAudioDevList(CSOUND* csound, CS_AUDIODEVICE[] devices, int isOutput)

let numberOfDevices = csoundGetAudioDevList(csound, [||], 1)
let mutable devices = [| for _ in 1..numberOfDevices -> CS_AUDIODEVICE() |]
csoundGetAudioDevList(csound, devices, 1) |> ignore
devices

According to Microsoft's own documentation and https://stackoverflow.com/a/8759368/17800932 this should just work, but it doesn't. It crashes. I don't know what it crashes, as I just get Session termination detected in FSI (F# interactive).


Attempt 2

I tried redefining my struct:

[<Struct; StructLayout(LayoutKind.Sequential)>]
type CS_AUDIODEVICE =
    [<MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)>]
    val device_name: char[] // char[64]
    [<MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)>]
    val device_id:   char[] // char[64]
    [<MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)>]
    val rt_module:   char[] // char[64]
    val max_nchnls:  int
    val isOutput:    int

The rest of the setup is the same. And the result is the same in that this crashes.


Attempt 3

In the past, when I have had difficult marshaling things in a straightforward way, I have usually turned to IntPtr. I tried variations of

[<DllImport(csoundDLLPath, CallingConvention = CallingConvention.Cdecl)>]
extern int csoundGetAudioDevList(CSOUND* csound, IntPtr devices, int isOutput)

let numberOfDevices = csoundGetAudioDevList(csound, IntPtr.Zero, 1)
let mutable pointer = IntPtr(numberOfDevices * Marshal.SizeOf(CS_AUDIODEVICE))
let mutable devices = [| for _ in 1..numberOfDevices -> CS_AUDIODEVICE() |]
Marshal.StructureToPtr(devices, pointer, true)
csoundGetAudioDevList(csound, pointer, 1) |> ignore
//for i in 1..numberOfDevices do
//    devices[i-1] <- Marshal.PtrToStructure<CS_AUDIODEVICE>(pointer)
//    pointer <- IntPtr.Add(pointer, Marshal.SizeOf<CS_AUDIODEVICE>())
devices <- Marshal.PtrToStructure<CS_AUDIODEVICE[]>(pointer)
devices

I was trying to follow https://stackoverflow.com/a/27483917/17800932 here plus things I have done before.


I simply cannot figure this out. Most things just silently crash, some things return stack traces that I can't understand, and some things even explicitly crash the CLR.

One frustration I have is that I cannot find a way to simply reason about this and read documentation on .NET and/or Csound's API. I do not have control over the API, only the documentation I linked above, so I can't just modify the function in the DLL that I am trying to call.

In my experience with these DLL bindings is that things that should work don't always, and I have found that with DLLImport stuff, I just gotta try sensible things and eventually get something working. So any explanations that not only provide something that works but explains why/how would be super appreciated!

Main questions:

  1. How should the CS_AUDIODEVICE struct be defined?
  2. What is the proper DLLImport declaration of the csoundGetAudioDevList function?
  3. How do I marshal the list of audio devices out of the DLL?
  4. Why/how does this all work?

Thank you!


Here is the rest of the code if you want to try and actually reproduce this. You need the Csound API. On Windows, it is located at Csound-6.18.1-windows-x64-binaries\build\Release\csound64.dll in the Windows binaries download.

open System.Runtime.InteropServices

[<Literal>]
let csoundDLLPath = @"<path to Csound DLL>"

[<Struct>]
type CSOUND = struct end

[<Struct; StructLayout(LayoutKind.Sequential)>]
type CS_AUDIODEVICE = ???

[<DllImport(csoundDLLPath, CallingConvention = CallingConvention.Cdecl)>]
extern CSOUND* csoundCreate()

[<DllImport(csoundDLLPath, CallingConvention = CallingConvention.Cdecl)>]
extern int csoundStart(CSOUND* csound)

[<DllImport(csoundDLLPath, CallingConvention = CallingConvention.Cdecl)>]
extern int csoundSetOption(CSOUND* csound, string option)

[<DllImport(csoundDLLPath, CallingConvention = CallingConvention.Cdecl)>]
extern int csoundGetAudioDevList(CSOUND* csound, ???, int isOutput)

let c = csoundCreate()
csoundSetOption(c, "-odac") // this enables using the system's audio devices as the Csound output device
csoundStart c
// This is where csoundGetAudioDevList would be called
9
  • 1
    In C language char is 1-byte, while in dotnet char is 2-byte. Maybe it's the problem? Try to change constant size from 64 to 32 Commented Jun 28, 2023 at 5:00
  • I tried Attempt 1 in a console app and it ran without crashing, but listed no devices. Have you tried running a console app instead of F# interactive? Commented Jun 28, 2023 at 5:03
  • Note: in C# CS_AUDIODEVICE can be declared in following way: sharplab.io/… Commented Jun 28, 2023 at 5:03
  • 1
    Does int in this particular C and int in C# have the same size? (Try with int16 too.) Is Pack needed in StructLayout? Is the size of your struct what you expect? Is Cdecl the correct convention? Commented Jun 28, 2023 at 5:14
  • [<Struct; StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)>] That will automatically make it 1-byte characters. You don't need to specify Pack, and you don't neeed to change SizeConst=64 to 32 Commented Jun 28, 2023 at 10:19

2 Answers 2

3

As mentioned in the comments the following should work to declare the struct from C#:

[StructLayout(LayoutKind.Sequential)]
public unsafe struct CS_AUDIODEVICE
{
    public fixed byte device_name[64];
    public fixed byte device_id[64];
    public fixed byte rt_module[64];
    public int max_nchnls;
    public int isOutput;
}

Unfortunately, F# doesn't support the fixed syntax for declaring inline buffers. Instead, we can do:

#nowarn "9"
#nowarn "51"

open System
open System.Runtime.InteropServices
open System.Runtime.CompilerServices
open Microsoft.FSharp.NativeInterop
        
type CSOUND = struct end

[<Struct; StructLayout(LayoutKind.Sequential, Size = 64)>]
type Byte64 =
    val mutable first: byte

[<Struct; StructLayout(LayoutKind.Sequential)>]
type CS_AUDIODEVICE =
    [<FixedBuffer(typeof<byte>, 64)>]
    val mutable device_name: Byte64
    [<FixedBuffer(typeof<byte>, 64)>]
    val mutable device_id: Byte64
    [<FixedBuffer(typeof<byte>, 64)>]
    val mutable rt_module: Byte64
    val mutable max_nchnls: int
    val mutable isOutput: int

[<DllImport("csoundDLLPath", CallingConvention = CallingConvention.Cdecl)>]
extern int csoundGetAudioDevList(CSOUND* csound, CS_AUDIODEVICE* devices, int isOutput)

let writeExample () =
    let mutable x = CS_AUDIODEVICE()
    let addr = &&x.device_name.first
    let span = Span<byte>(NativePtr.toVoidPtr addr, 64)
    span[0] <- byte 'a'
    span[1] <- byte 'b'
    span[2] <- 0uy

    printfn "%s" (Marshal.PtrToStringAnsi (NativePtr.toNativeInt addr))

    
let callExample () =
    let xs = GC.AllocateArray<CS_AUDIODEVICE>(3, pinned = true) // Array must be pinned at allocation or with a GCHandle
    let mutable csound = CSOUND()
    let code = csoundGetAudioDevList(&&csound, &&xs[0], 0)
    code

We have to coerce the field addresses into a VoidPtr then we can use Span as a way to access the reserved bytes. Allocating a pinned array allows us to pass in the address to the native function.

Attempting to do this without first pinning the memory will likely crash or corrupt the program. Structs allocated with let mutable will live on the stack and it is safe to take their address while they are in scope.

Edit (Additional Explanation):

FixedBuffer isn't necessary but is emitted by the C# version (as seen when decompiling with SharpLab). I didn't test it but it should allow C# consumers to treat Byte64 as a byte[].

MarshalAs attributes cause the compiler to generate extra marshaling code behind the scenes to convert the .NET type into a native compatible type. The way it is done with Byte64 the struct is unmanaged ref1 ref2 and no additional marshaling code is needed.

Span isn't strictly necessary and you can freely convert between IntPtr and nativeptr<_> using the NativePtr module functions. This module gives you ways to do pointer manipulation directly. IntPtr.Zero is equivalent to NativePtr.nullPtr and nativeptr<_> gets erased to IntPtr by the F# compiler.

Sign up to request clarification or add additional context in comments.

6 Comments

Thanks! A couple of questions. (1) Why is FixedBuffer needed and what does it do differently than [<MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)>] val device_name: byte[]? (2) Is there a way to make this work with IntPtr instead of using NativePtr and Span? When I use IntPtr, I can get the number of devices just fine by passing in IntPtr.Zero for a null pointer, but I get crashes of Fatal error. System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt. when trying to get the devices.
I've added some additional explanations. If you're still getting AccessViolationException there's likely something else wrong with the pointer arithmetic. This kind of code is very error-prone and I tend to write lots of tests and helper functions to ensure all the bytes are laid out correctly.
Thanks so much! And yes, this stuff is quite delicate with not great documentation from Microsoft and somewhat reliant upon the DLL to behave expectedly. I will give this another try. Also, F# does have a fixed keyword which can "fix" a value and return a nativeptr. I tried doing something similar to what you did by using fixed on an array of CS_AUDIODEVICEs and then marshaling it similar to what you did with Span, but it didn't work, even though the size (in bytes) of the array is what I would expect (bytes per struct = 196 = 3*64 + 2 + 2).
Thanks again for the help! I finally got this going. The primary problem was that the Csound API documentation is out of date. :( I still wasn't getting any solution, including yours, working, but at least yours was returning some data and not crashing. After that, I inspected the Python bindings for the Csound API, which is in the Csound repository, whereby I discovered that the size of the char arrays is actually 128 and not 64. Your solution now works perfectly. I'm going to play around with it and see if I can't simplify it to where some of the marshaling is handled more automatically.
Thanks once more for the help and solution! I played around a bit more after figuring out the documentation error and having your solution to go off of and came up with another solution. Thanks again!
|
2

It turns out that one of the main issues in figuring all of this out properly is that the Csound API documentation is out of date. If we view the CS_AUDIODEVICE struct in the API documentation, then we see that the char arrays are of length 64. However, if we navigate to the Csound repository, the header lists the arrays in CS_AUDIODEVICE as length 128. This is unfortunate, since it means I and others here were spinning wheels on something that couldn't possibly work due to the documentation error.

Thankfully, due to @tranquillity's answer, I was able to get some actual data back with their method (instead of just crashes) and noticed that the data was incomplete. I then looked up the Python Csound API bindings, which happen to be in the Csound repository itself, which is where I saw the 128 length and then confirmed it in the csound.h header file.

Since there are a lot of the structures like this in the Csound API, I am wanting something a bit more straightforward, so I played around a bit and come up with:

open System
open System.Runtime.InteropServices

[<Struct>]
type CSOUND = struct end

[<Struct; StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)>]
type CS_AUDIODEVICE =
    [<MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)>]
    val device_name: string // char[128]
    [<MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)>]
    val device_id:   string // char[128]
    [<MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)>]
    val rt_module:   string // char[128]
    val max_nchnls:  int
    val isOutput:    int

[<DllImport(csoundDLLPath, CallingConvention = CallingConvention.Cdecl)>]
extern int csoundGetAudioDevList(CSOUND* csound, IntPtr devices, int isOutput)

let marshalIntPtrToStructures<'Structure> (pointer: IntPtr, numberOfStructures) =
    let mutable pointer = pointer
    [| for _ in 1..numberOfStructures ->
        let structure = Marshal.PtrToStructure<'Structure>(pointer)
        pointer <- IntPtr.Add(pointer, Marshal.SizeOf<'Structure>())
        structure |]

let initializeIntPtrFromStructures<'Structure> numberOfStructures: IntPtr =
    Marshal.AllocHGlobal(numberOfStructures * Marshal.SizeOf<CS_AUDIODEVICE>())

let getAudioDeviceOutputs (Csound csound) =
    printfn "Size of CS_AUDIODEVICE: %A" (Marshal.SizeOf<CS_AUDIODEVICE>())

    let numberOfDevices = csoundGetAudioDevList(csound, IntPtr.Zero, 1)

    printfn "Number of devices: %A" numberOfDevices
    
    let pointer = initializeIntPtrFromStructures<CS_AUDIODEVICE> numberOfDevices

    csoundGetAudioDevList(csound, pointer, 1) |> ignore

    let devices =  marshalIntPtrToStructures<CS_AUDIODEVICE>(pointer, numberOfDevices)

    for device in devices do
        printfn "Name: %A, ID: %A" device.device_name device.device_id

The printfn statements are there to confirm the values are what I expect. This all works now! And this should be generally reusable across the several structs in the Csound API (I hope). It also allows me to define the struct in a more straightforward way and makes it easier to marshal out the fields once I have the array of structs in hand.

What I am still curious about though is if there is a better and/or built-in way to do what the marshalIntPtrToStructures and initializeIntPtrFromStructures are doing. It seems kind of strange that there isn't, at least I couldn't find them, especially for marshalIntPtrToStructures. From what I have seen, you cannot use the built-in Marshal.PtrToStructure on an array. I was getting unsupported type exceptions like: System.MissingMethodException: Cannot dynamically create an instance of type 'FSI_0028.Csound.Native+CS_AUDIODEVICE[]'. Reason: Type is not supported. (I think that is the right exception from when I tried that).


A final note. Microsoft has documentation for marshaling arrays and structs here:

The problem with those methods is that the csoundGetAudioDevList is a bit weird and requires a null pointer to be passed in to first get the number of audio devices. Then you call it again with a properly constructed pointer to get the actual devices. With the methods described in the two bullets above, there is no way to pass in a null pointer with the way that the prototypes are declared, as far as I can tell. Those methods are the easiest since they defer a lot of the marshaling (to the CLR, I guess), but they don't work for an unmanaged function like csoundGetAudioDevList that also requires use of a null pointer.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.