#C++ ABI Interop with C, Strings issue

50 messages · Page 1 of 1 (latest)

dark barn
#

Hey guys, I'm currently trying to write a translation layer for the Steamworks SDK in pure C.

This SDK relies on C++ headers to link against, but has a C ABI included in the library. My goal has been to write a generator for a pure C header, including all the data structures needed to interface with the C ABI.

As part of my research for this project, I've run into an issue with calling C++ C ABI functions which return strings, illustrated in the example below:

// libabi.so
extern "C" {
    const char* getMessage() {
        return "Hello from the dynamic library!";
    }
}

// abi_test.c
int main() {
    const char *message = getMessage();
    printf("Message: %s\n", message);
    return 0;
}

(I have also tested this function returning a malloced char *, but the same problem occurs)

When running abi_test.c (properly linked against the .so), the memory at the pointer returned from getMessage() is corrupted.

(gdb) print message
$1 = 0x555592a0 <error: Cannot access memory at address 0x555592a0>

In my sanity checking, I tested creating a new C++ file to wrap the C++ ABI, and expose it as extern "C" within the same project as the test, and observed the memory being returned from the library un-corrupted in C++, and the same behavior with the pointer memory corrupting once it returns to C from the forward declaration.

I hope this isn't too much of a word soup, and I'm happy to clarify anything... This is a minimum recreated example of what I've experienced testing the Steamworks SDK directly, and if it helps I can upload a full CMake project example

surreal ibexBOT
#

When your question is answered use !solved to mark the question as resolved.

Remember to ask specific questions, provide necessary details, and reduce your question to its simplest form. For tips on how to ask a good question use !howto ask.

flint stratus
#

This is a minimum recreated example
I love you

dark barn
#

Based on the ABI spec, they describe the return type as const char *, ex:

        {
          "methodname": "GetPersonaName",
          "methodname_flat": "SteamAPI_ISteamFriends_GetPersonaName",
          "params": [],
          "returntype": "const char *"
        },

I'm assuming that they are ensuring you have at least some amount of time to copy a heap string into whatever you want

#

Also that snippet is indeed broken, since its returning a stack string, but just to illustrate a heap example:

// libabi.so
#include <cstring>
#include <cstdlib>

extern "C" {
    const char* getMessage() {
        const char* message = "Hello from the dynamic library!";
        char* heapMessage = (char*)std::malloc(std::strlen(message) + 1);
        std::strcpy(heapMessage, message);
        return heapMessage;
    }
}
Breakpoint 1, getMessage () at /home/ryan/repos/pure-c-steamworks/abi_demo/lib/src/main.cpp:6
6               const char* message = "Hello from the dynamic library!";
(gdb) n
7               char* heapMessage = (char*)std::malloc(std::strlen(message) + 1);
(gdb) 
8               std::strcpy(heapMessage, message);
(gdb) 
9               return heapMessage;
(gdb) print heap
heapMessage  heap_info    
(gdb) print heapMessage 
$1 = 0x5555555592a0 "Hello from the dynamic library!"
(gdb) n
10          }
(gdb) 
main () at /home/ryan/repos/pure-c-steamworks/abi_demo/test/src/main.c:7
7           printf("Message: %s\n", message);
(gdb) print message
$2 = 0x555592a0 <error: Cannot access memory at address 0x555592a0>
#

My goal is to get as close to the native C ABI as possible for the Steamworks SDK, so ideally I could just write a generator to re-declare the existing C ABI function signatures and datatypes...

I'm assuming that valve would not go through the trouble of exposing a C ABI, just to make it inaccessible from pure C... It could very much be the case though

#

I have tested exactly that though, but the implication of that is I need to write a more complex generator and change how the API is normally used

#

if you have a minimum example for C++ C string return interop that would be awesome

#

for this I have no header, I'm just implicitly linking against the sig

#

but it's resolving properly, its just that the pointer memory corrupts once back in C

#

is it even possible that the virtual address space is different for the lib and its getting unmapped when returning back to the main program?

#

no (to the static link):

libabi

  • implemented in C++ with extern "C"
  • shared library

demo

  • links against libabi.so
#

sounds good, if you could keep it platform independant that would be awesome

#

I appreciate it

dark barn
dark barn
#

hmmm... this does indeed work for me

dark barn
#

So, I've been trying to apply this to the steamworks SDK again, and I've found that with a C++ wrapper just handing off the pointer to C I can get it to work, but not without such a wrapper...

working:

//internal.h
const char* GetPersonaName(void);

//bind.cpp
extern "C" const char* GetPersonaName()
{
    const char* personaName = SteamFriends()->GetPersonaName();
    return personaName;
}

//demo.c
#include "internal.h"
int main() {
    bool result = SteamAPI_InitSafe();
    printf("SteamAPI_InitFlat() result: %d\n",result);
    if(!result) {
        printf("SteamAPI_InitFlat() failed\n");
        return 1;
    }

    char * personaName = GetPersonaName();
    printf("Persona name: %s\n",personaName);

    SteamAPI_Shutdown();

    return 0;
}
#

This is confusing, because I would expect to just be able to do something like this:

//internal.h
const char * SteamAPI_ISteamFriends_GetPersonaName(void);

//demo.c
#include "internal.h"
int main() {
    bool result = SteamAPI_InitSafe();
    printf("SteamAPI_InitFlat() result: %d\n",result);
    if(!result) {
        printf("SteamAPI_InitFlat() failed\n");
        return 1;
    }

    printf("Persona name: %s\n",SteamAPI_ISteamFriends_GetPersonaName());

    SteamAPI_Shutdown();

    return 0;
}

which does not work at all...

#

but I guess technically the wrapper is not using the C ABI signature...

dark barn
#

That is a C ABI function exported in the steamworks SDK, of which the source code is not available.

the spec:

        {
          "methodname": "GetPersonaName",
          "methodname_flat": "SteamAPI_ISteamFriends_GetPersonaName",
          "params": [],
          "returntype": "const char *"
        },

There are headers included with the SDK for C++, and there is a "flat" header which describes a C ABI, of which the signatures dont appear to want to link. All of the flat symbols can be observed with nm or readelf, so I know they exist (and I can use the flat init function)

#

Yeah the end goal is basically to parse that json into a pure C header so I have the declarations, but these smaller tests I'm doing now with the manual forward declarations are failing

#

hmm

#

statically linking isnt an option afaik

#

hmmm... surely theres a way for the linker to automatically resolve these when compiling, no?

#

hmm... for windows they provide a .lib but I'm not aware of an equivalent on linux

#

hmm

#

worth a shot i guess

#

I took a mega long shot earlier and opened a steam support ticket, its kinda ridiculous how hard it is to get dev support... the only community is a steam group which has been closed (max members) for around a decade

#

The guy who used to maintan CSteamworks (a C ABI before valve shipped one with the SDK) has no contact information, other than his linkedin

#

pretty sure he is also the reason they ship the json spec, since he worked at valve for a year and rewrote their docs and improved the generators

#

I do and that’s the plan… but I can’t get the ABI to work in the first place, so the header would be useless…

#

Just seems like the linker already sees the symbols so why can’t it resolve them in the ELF?

#

You’re right though

#

no dice

    #include <dlfcn.h>

    const char *(*SteamAPI_ISteamFriends_GetPersonaName)(void);

    void *handle = dlopen("../lib/libsteam_api.so", RTLD_NOW);
    if (!handle) {
        fprintf(stderr, "Error opening library: %s\n", dlerror());
        return 1;
    }

    dlerror(); // Clear any existing error

    *(void **)(&SteamAPI_ISteamFriends_GetPersonaName) = dlsym(handle, "SteamAPI_ISteamFriends_GetPersonaName");
    const char *dlsym_error = dlerror();
    if (dlsym_error) {
        fprintf(stderr, "Error loading symbol 'SteamAPI_ISteamFriends_GetPersonaName': %s\n", dlsym_error);
        dlclose(handle);
        return 1;
    }

    printf("Successfully loaded 'SteamAPI_ISteamFriends_GetPersonaName' function.\n");

    const char *personaName = SteamAPI_ISteamFriends_GetPersonaName();
    if (personaName) {
        printf("Successfully retrieved persona name: %s\n", personaName);
    } else {
        printf("Failed to retrieve persona name.\n");
    }

    dlclose(handle);

    return 0;
#

must be the impl inside the lib

#

the flat header itself is quite odd, since it has the class the function belongs to in C++ as a parameter (which is not described in the json spec)

S_API const char * SteamAPI_ISteamFriends_GetPersonaName( ISteamFriends* self );
#

idek. I'll leave it alone for today I guess

dark barn
#

what if you try to inkoke it after?

#

it resolves for me too

#

The invocation is the segfault

#

It relies on C++ headers, the json describes the C ABI

#

I don’t believe so, because the C++ api doesn’t, but maybe init_flat() is fundamentally different

#

Thanks for trying

surreal ibexBOT
#

@dark barn Has your question been resolved? If so, type !solved :)

dark barn
#

Gonna mark as resolved since the original question has been answered

#

Thanks again @regal hill

#

!solved

surreal ibexBOT
#

Thank you and let us know if you have any more questions!

This thread is now set to auto-hide after an hour of inactivity

dark barn
#

update:
my goal would be impossible without wrapping certain things extensively in C++

this conversation with the SDK Docs author makes that pretty clear:
https://github.com/rlabrecque/CSteamworks/issues/11

and I found a hidden repo actually attempting to do so (incompletely):
https://github.com/EvilPudding/steam.candle
Also I opened a ticket with steam support (in a totally non developer channel) who did not have anything to add, other than telling me to get in contact via github (where there is no relevant repo) lol