Problem Marshaling C char* to string

Hello all. I’m trying to make use of a C program compiled to dll through Unity C#.
The code is rather simple, yet it doesn’t work.

When putting this on an object in Unity, running produces a Unity crash. I suspect it has to do with the malloc in the C code, as if I had simply char* ptr = “Hello World” Unity doesn’t crash, but I only get junk. I never get the string from C. Also, I recognize that I will have to free the memory allocated by the C program , but I will add that later.

Where have I gone wrong with this? I have the malloc in there because this is a test for a library which allocates some space and returns char*.

Thank you for your help with this.

testdll.h

#ifdef TESTDLL_EXPORTS
#define TESTDLL_API __declspec(dllexport)
#else
#define TESTDLL_API __declspec(dllimport)
#endif

TESTDLL_API char* fntestdll(void);

testdll.c

#include "pch.h"
#include "framework.h"
#include "testdll.h"

// This is an example of an exported function.
TESTDLL_API char* fntestdll(void)
{
    char* ptr = (char*)malloc(sizeof(char) * 12);
    strcpy_s(ptr, 12, "Hello World");
    return ptr;
}

Within Unity, UseTestDll.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.InteropServices;
using System;

public class UseTestDll : MonoBehaviour
{
#if UNITY_IPHONE
       [DllImport ("__Internal")]
#else
    [DllImport("testdll")]
#endif

    private static extern IntPtr fntestdll();

    void Awake()
    {
        IntPtr ptr = IntPtr.Zero;
        ptr = fntestdll();
        string results = Marshal.PtrToStringAnsi(ptr);
        Debug.Log(results);
    }
}

On the C side, is condition ptr[11] == ‘\0’ always true before returning that pointer to c# side? And what is TESTDLL_API ? It may require applying addition marshalling attributes on c# side.

Here is where I expose my ignorance. When attempting to add the null character (via below) Unity still bombs.

Regarding TESTDLL_API:
it’s defined as “#define TESTDLL_API __declspec(dllexport)”
and I know pretty much nothing about that other than I think it’s some kind of framework used by visual studio 2019 to export the code as a library.

TESTDLL_API char* fntestdll(void)
{
    char* ptr = (char*)malloc(sizeof(char) * 12);
    strcpy_s(ptr, 12, "Hello World");
    *(ptr + 11) = '\0';
    return ptr;
}

This is how I return strings from a native C function to Unity. Works in iOS, Android, MacOSX and Windows.

Look for the first blurb of C# side code in this post:

Thank you for the link. As far as I can tell, the only difference between your function definition and mine is you use System.IntPtr and I was just using IntPtr. I assume those are the same thing, but perhaps in my ignorance I’m wrong. In your example I don’t see you actually using dispatcher1_entrypoint() which is the function that returns the string. I’m curious exactly how you are Marshalling it. If you are really just Marshal.PtrToStringAnsi(); then that is what I’m doing as well.

Let me fill in a bit more code from my KurtMaster2D game.

This is the C code function prototype:

DECORATE_FOR_DLL    char            *dispatcher1_entrypoint1( int opcode1, int arg1);

I think in the end DECORATE_FOR_DLL was blank for all targets.

This is the C# call site for that function:

string result = Marshal.PtrToStringAnsi(
            dispatcher1_entrypoint1( (int)opcode, arg1));

And again, this is the interop declaration for iOS:

    [DllImport ("__Internal")]
    private static extern System.IntPtr dispatcher1_entrypoint1( int opcode1, int arg1);

Other targets are slightly different DLLImport decorators, as outlined in the Unity interop manual.

Another key point is that if you are returning a local variable from your C function, that is gonna blow up. Local variables in C exist only on the stack for the lifetime of the call to the function. When the function returns, the local variable is gone.

You can return:

  • static local variable such as char tmp[2048];
  • static global variable (they’re the same)
  • a chunk of memory that you malloc() or strdup()

DON’T try to deallocate things on the Unity side; manage the memory you allocate in native on the native side.

I just declare a 2k block of static space (the above char tmp[2048]; ) and return that address every time. Each subsequent call to native wipes out what was there before, as this only reports the status of each call.

EDIT! CORRECTION: For Win32 targets, this is set for the DLL decorator in C:

-DDECORATE_FOR_DLL="__declspec(dllexport)"

Again I think all the other targets are set to empty #define but I’m not 100% sure.

Does it only crash in the build or in editor as well?

First things first: Is this compiled using a C compiler or C/C++ compiler?
Make sure the names of the plugins functions are not name-mangled, i.e. they’re the same as you read them in the source code. Using a C/C++ compiler, you may need to use extern “C” in order to avoid (or at least limit) name-mangling, or you need to inspect the compiled dll and find the corresponding mangled name. Otherwise, you’ll get an exception complaining about an entry point that cannot be found.

As an additional note, some of information that’s in the header file doesn’t need to be repeated in the .cpp file. Add the export/import symbols in the header. This should work just as fine. Same applies extern “C”, if required.

Next thing to check is whether the architecture (x86, x64 … ) matches the architecture of your desired platform. You may need to supply different versions for platforms and configure them, i.e. for which platforms they’re meant to be used. Simply click on the plugin and the inspector should present the available settings.

There are two major problems with your code.

  1. As someone else said, you don’t have a null terminator on your C string.

  2. You are giving an unmanaged pointer to a managed language. You can’t do that because at some point, the C# will attempt to deallocate the memory, which it won’t be able to do. Marshal.PtrToStringAnsi has a second version that takes the length as a parameter, the documentation says that this version will make a copy of the data, so your pointer is safe. You do have to write another DLL function to deallocate anything that you allocated from it.

TESTDLL_API char* dllfree(char * ptr){ free(ptr); }

While there is some weird fuckery you can do with memory, the rule of thumb is that whatever allocated it needs to deallocate it (you also have to do it the right way so make sure you know what you malloc and what you new.)

Given that he copies “Hello World”, which is a string literal, it is already null-terminated. In C and C++ string literals are always null-terminated. The way he copies the string in this particular case includes the null-terminating char, so even the extra line for the assignment seems to be a redundant step.

Why would “C#” (I guess you meant the runtime) attempt to reclaim that memory? He allocates the memory using malloc, so the proper way to free the memory is exposing a function that frees the memory again (he said he’s going to do that later).

Actually this is not true. If you don’t TRY to deallocate it on the C# side you’ll be fine. The C# side could of course deallocate the System.IntPtr object when it falls out of scope but not what it points to.

The original code he posted just leaks 12 bytes of memory every call, and since “Hello World” is 11 chars and he puts it in a fresh 12-byte array, pretty sure strcpy_s will tack the zero in the end too. I think he was just posting demo code.

1 Like

Suddoha, thank you for the information. I will try to answer some of your questions:

Crash on build or in editor? I’m seeing this crash in the editor. I didn’t actually try to run a build. Perhaps I should do that.

C or C++ Compiler? As far as I’m aware this is straight C code using the Visual Studio C++ build utilities which includes a C compiler, but I think the answer to your question is C/C++ compiler.

Name Mangling? I believe this function is not name-mangled, as I thought C does not name-mangle. Indeed, if I use the 'extern “C” {} ’ I get an error in the (C) compiler that it is unrecognizable. My understanding is that 'extern “C” { } ’ only works with C++. Also, I do not receive an error about the entry point. Perhaps it’s worth mentioning that if I change the return type from char* to int, and simply have the function return a number everything works.

Architecture? I discovered that if I build the dll for x86 then Unity complains directly about the architecture mismatch; that was my first mistake. To answer your question the dll build does match the architecture I’m using in Unity.

newjerseyrunner thanks for trying to help me out with this! Given your suggestion; let me try to implement the alternative version of Marshal.PtrToStringAnsi where one can pass in the length and see what happens.

In response to the discussion about the reclaiming memory – it is my understanding that I need to reclaim in the memory on the C side, but I thought it was unnecessary at this point as I’m just testing the initial C to C# return of char*. I absolutely will provide a function to reclaim that memory at some later point, and I have found several examples that do this.

In response to the question about the code being “demo”. Absolutely, this is just demo code. I wanted to make a basic step or two so I understand how this works, before jumping into the real C C# code.

It seems that a lot of other people have this working, so I feel like I might be making some very basic mistake here.

newjerseyrunner

Changing the code to

        string results = Marshal.PtrToStringAnsi(ptr,12);

produces no difference in behavior. Unity still crashes. Weird.

To everyone: I did discover something

If I change the (C) code to the following:

TESTDLL_API char* fntestdll(void)
{
    char* ptr = "Hello World";
    return ptr;
}

THIS WORKS. I get “Hello World” on the C# side. So what is wrong about the way I’m allocating the memory? Here is the original code for reference

TESTDLL_API char* fntestdll(void)
{
    char* ptr = (char*)malloc(sizeof(char) * 12);
    strcpy_s(ptr, 12, "Hello World");
    *(ptr + 11) = '\0';
    return ptr;
}

By returning malloc-ed C string you are leaking memory, since the allocated memory is never freed.
Better pass C# byte array as argument to this function with [MarshalAs(UnmanagedType.LPArray)] attribute and fill it in C code. Then on C# code convert the bytes to string.

Here’s an example (C# to Objective-C, but the interop layer is in C):
https://github.com/Unity-Technologies/BackgroundDownload/blob/master/BackgroundDownload/BackgroundDownloadiOS.cs
https://github.com/Unity-Technologies/BackgroundDownload/blob/master/Plugins/iOS/BackgroundDownload.mm

Aurimas-Cernius

Thank you for your help with this. I do understand that my code produces a memory leak, is bad in practice, and that the memory needs to be freed. I just wrote a very simple demo.
I guess I don’t see why the leak would cause Unity to crash outright. Do you think not freeing the memory is the cause of the crash?

You do bring up a good point though. Is it better in practice to allocate the memory and free it on the C# side or the C side?

Not freeing the memory is not the cause.
You can try casting IntPtr to byte* like this:
https://stackoverflow.com/questions/713324/how-to-cast-intptr-to-byte

I’d go with allocating C# byte array and pass it to C code for filling, then convert bytes to string in C#.

I’d make it work in the editor first.

Yes, you’re right. I missed that your file is a .c file, not a .cpp. So it follows the rule set of C and is compiled as such I guess.

Now that could be the game changer.

Note that in C, you’re not required to upcast pointers in this case, and according to discussion and information on the web (linked below), you shouldn’t do that unless you are absolutely aware of what you’re doing there.
In contrast, implicit assignment is not allowed in C++, and you’ll need a cast, otherwise an error pops up.

This detail might be important, because you also have additional headers there, and we don’t really know anything about the content.

So as we’re dealing with C, and not C++, let’s pay attention to this line:

char* ptr = (char*)malloc(sizeof(char) * 12);

The return type on malloc depends on whether you have specific headers included.
See this StackOverflow answer for clarification.
See this FAQ (linked in that answer as well) for another description.

Basically, without including “stdlib.h”, malloc’s return type is assumed to be “int” due to implicit return type declaration, whereas with the proper header included, it is declared as being void*, i.e. a pointer type.

So what could happen is:

  1. malloc allocates memory, and since we’re compiling for x64, it’ll utilize addresses which are 8 byte
  2. malloc would return that 8 byte pointer, but it is called as if it was returning an int, which happens to be 4 byte, so it actually returns just half of those 8 byte, i.e. 4 bytes are cut off and only 4 bytes are actually returned.
  3. your code then takes that truncated address, of which only 4 byte remained, and it casts that value to a pointer, which again happens to have a size of 8 bytes, since we’re compiling for an x64 architecture. The other 4 bytes will then either be zeros or just arbitrary junk.

So there’s potentially a risk of a loss of information, the subsequent copy-operation might attempt to copy the string to an address that it shouldn’t be writing to, as it misses those original 4 bytes of address information. This could lead to uncontrolled memory access, i.e. access violation. You might see unexpected behaviour or crashes.

That being said, check what is currently the return type of your malloc call. If none of the included headers includes “stdlib.h”, it should be “int”, which is not what you want. If you’ve (already) included that header, it should be “void*”, which is what you want.

If that’s not the case, we can still dig little deeper.

1 Like

Try to isolate as many variables that you can.

First, have your function just return null and not do anything with it in c#. Maybe explicitly define then as __stdcall rather than relying on the definitions.

Make a static char string in your library and return a pointer to that. Printf the value of the pointer you are returning, is the same value being propagated?

Forget trying to change it to a string, just hex dump the first 12 bytes that the returned pointer equates to.

Also, something that I like to do when debugging libraries is to not link them but explicitly load them using dlopen (or whatever the windows equivalent is.). IDEs tends to put builds in weird places and dll loading is a weird priority hierarchy of search paths and this way i know I’m always loading the correct dll. Did you check the simple things like your linker paths? You’re compiling for x64 right?

Horay! It works!

The cause:
I was, indeed, missing ‘#include <stdlib.h>’ in the C file as Suddoha exquisitely suggested. When hovering over the malloc function it reported a return of simply ‘int’. Adding the include produced a reported return type of void*, and fixed the issue. I suspect the code was causing an access violation just as Suddoha says above. Here is the resulting, working code. I get “Hello World” on the C# side as expected.

#include "pch.h"
#include "testdll.h"
#include <stdlib.h>

// This is an example of an exported function.
TESTDLL_API char* fntestdll(void)
{
    char* ptr = malloc(sizeof(char) * 12);
    strcpy_s(ptr, 12, "Hello World");
    return ptr;
}

I have removed the superfluous nul terminator and the malloc cast to char* as suggested by Suddoha’s linked FAQ (thank you very much for that).

What I learned:

While troubleshooting, I had suspected the problem was the malloc call, but I either lacked the initiative or the knowledge to dig deeper and carefully compare its return with what I was expecting. I never checked the docs for malloc to better understand its behavior – because I thought I knew. Now I realize next time I should dig deeper before I pester you guys with questions; sacrificing your precious time.

So this is awesome! Now that I have made my first tentative steps I can start working on the real code.
I want to thank you all very, very much for your kind, patient guidance while helping me through this. C is a tricky tricky thing, it seems.

Thanks again!

2 Likes

So, it seems you haven’t learned the important thing: read compiler warnings! Even better - make warnings as errors.

1 Like