4

I'm trying to figure out how to return a complex object from a C++ dll to a calling C# application. I have a simple method which is returning an int that is working fine. Can anyone tell me what I'm doing wrong?

C# Application:

class Program
{
    static void Main(string[] args)
    {
        // Error on this line: "PInvoke: Cannot return variants"
        var token = LexerInterop.next_token();
    }
}

C# LexerInterop Code:

public class LexerInterop
{
    [DllImport("Lexer.dll")]
    public static extern object next_token();

    [DllImport("Lexer.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern int get_int(int i);
}

C++ Lexer.cpp

extern "C" __declspec(dllexport) Kaleidoscope::Token get_token()
{
    int lastChar = ' ';
    Kaleidoscope::Token token = Kaleidoscope::Token();

    while(isspace(lastChar))
    {
        lastChar = getchar();
    }

    ... Remainder of method body removed ...

    return token;
}

extern "C" __declspec(dllexport) int get_int(int i)
{
    return i * 2;
}

C++ Lexer.h

#include "Token.h"

namespace Kaleidoscope
{
    class Lexer
    {
    public:
        int get_int();
        Token get_token();
    };
}

C++ Token.h

#include <string>

namespace Kaleidoscope
{
    enum TokenType
    {
        tok_eof = -1,
        tok_def = -2,
        tok_extern = -3,
        tok_identifier = -4,
        tok_number = -5
    };

    class Token
    {
    public:
        TokenType Type;
        std::string IdentifierString;
        double NumberValue;
    };
}

I'm sure it's something to do with the Token return type but I can't find any decent information what's missing.

Any assistance or direction is appreciated!

Nate222
  • 856
  • 2
  • 15
  • 25

1 Answers1

6

You cannot return C++ objects to non-C++ callers. (It doesn't even work when you try to export a C++ object to a caller if it's compiled with a different C++ compiler.) The reason is that the actual in-memory layout of the C++ object is not standardized and different compilers may do it differently.

The .NET runtime has no idea how to deal with your object and it doesn't know about the various types that make up your token object. Similarly, std::string makes no sense to .NET, since it's a C++ type, relying on unmanaged memory allocation and with a different internal string format and different semantics than C#.

You can try turning your C++ objects into COM objects and then you can return COM interfaces to your C# caller. This will take some work since COM types are, again, incompatible with C++ types (for similar reasons as above).

You can also try to serialize your C++ object into a byte array and then just return a buffer / length to C# that will first deserialize the object into a .NET representation. You'll have to duplicate the classes you're trying to handle this way.

Yet another possibility is that you return all data as independent out parameters from your function calls - that is, you don't try to return token but return each of its properties as a separate out parameter from C++. You'll still have to convert some of your data types to something recognizable by .NET. (std::string cannot be returned, you'll have to copy it to a byte array or allocate it as a BSTR, etc.)

Finally, a fairly involved option: you can return the address of token as a void* from your C++ function and then create a number of exported plain C functions to read the various properties based on an input pointer. You'd also need a function to free up the C++ object when you're done with it. Something like:

__declspec(dllexport) TokenType WINAPI GetTokenType ( Kaleidoscope::Token* pToken )
{
    return ( pToken->Type );
}

You'd then declare these functions in C# like so:

[DllImport ( "MyDll.dll" )]
public static extern int GetTokenType ( UIntPtr pObj );

The UIntPtr instance would be initialized from the void* that your next_token function returns. You'd then call each of the exported property reader functions to get the individual properties of token.

I'd probably go with a custom serializer because that's the least amount of work (in my opinion) and I think it's the cleanest solution (in terms of readability and maintainability).

Note: using COM objects could be less work but you'd have to move away from any C++ types (at least in the public interface).

Edit: I forgot to mention earlier a somewhat obvious option - using mixed code and C++/CLI. This way you can have unmanaged code and managed code at the same time in the same DLL and you can perform all conversions in C++ code. If you define most of your classes in C++/CLI, you can just pass the relevant (managed) instances to your C# application without additional conversion.

(You will, of course, still have to convert between std:string and System.String if you use the former but at least you can have all your conversion logic in the same single project.)

This solution is simple but the resulting DLL now requires the .NET runtime.

xxbbcc
  • 16,930
  • 5
  • 50
  • 83
  • Excellent. Thanks for the quick response. Would you happen to know of any examples online that gives an overview of serializing a C++ object? Either way, thanks. – Nate222 Jun 16 '13 at 21:42
  • 1
    @nDev Not off the top of my head - I don't think it can be automated but I may be wrong. I'd just write my own (you need a serializer in C++ and a deserializer in C#) - it's really just writing `int`-s and `string`-s into a byte array and then reading them back in the same order. – xxbbcc Jun 16 '13 at 21:44
  • 1
    @nDev Take a look at this question: http://stackoverflow.com/questions/523872/how-do-you-serialize-an-object-in-c – xxbbcc Jun 16 '13 at 21:46
  • @nDev Thanks for accepting my answer. I realized I forgot to mention using a mixed code DLL as one of the options you have so I updated the answer. – xxbbcc Jun 17 '13 at 00:38
  • @DavidHeffernan Yes, you're right, that's why I wrote 'managed' with lower-case 'M'. I never really made much difference between it and C++/CLI anyway. :) – xxbbcc Jun 17 '13 at 12:33