1

I wrote this simple piece of C code:

#include <string.h>
#include <emscripten.h>


EMSCRIPTEN_KEEPALIVE
const char * helloWorldStatic( ) { 
    return "Hello World from C";
}


EMSCRIPTEN_KEEPALIVE
char * helloWorldDynamic( ) { 
    return strdup("Hello World from C"); 
}

Now when I compile that to WASM32 using Emscripten, I can access either function from TypeScript like this:

async function helloWorldStatic( ): Promise<String>
{
    let module = await Module
    let result = module.ccall(
        "helloWorldStatic", // name of the C function
        "string",           // return type
        [ ],                // argument types
        [ ]                 // arguments
    )
    return result
}

This seems to work fine. When printing the string, I get the expected value.

But what about the memory management? I found nothing in the docs explaining the memory management when using ccall or cwrap the return value is a string.

Does Emscripten always assume all strings are static and never need to be freed? Does it assume they are dynamic and will always free them?

In this example it may assume that const char * needs no freeing, but even if totally lie about it:

EMSCRIPTEN_KEEPALIVE
char * helloWorldStatic( ) { 
    return "Hello World from C";
}

EMSCRIPTEN_KEEPALIVE
const char * helloWorldDynamic( ) { 
    return strdup("Hello World from C"); 
}

It still seems to work. I'm not sure if strings are freed and the code just doesn't crash when trying to free the constant string or if strings just leak that way.

4
  • AFAIK, when values leave or enter the WASM engine they are copied (and thus your strdup is not needed). So the value you see in JS is equivalent to making a normal js string. You would need to free it inside the WASM though. Commented Oct 31, 2024 at 9:05
  • @Cine But how can I dynamically create a string in C, return it and then free it in WASM? Once returned I have no access to that string anymore in the WASM code unless I store it to a global variable. That makes ccall/cwrap returning string pretty pointless as it can only be used for static strings then. Commented Oct 31, 2024 at 19:05
  • The point was the the memory space for WASM and JS are totally seperated. If you alloc in C, you should free in C. See also stackoverflow.com/questions/61795187/… Commented Nov 1, 2024 at 9:24
  • @Cine Yes, but then you can never use ccall or cwrap with a string return type as if you do that, you lose the only reference to the string you ever had in C and thus you cannot ever release that memory in C. Then you must do it as shown in my answer and return just a pointer and create the JS string yourself. This means the string return type of ccall/cwrap can only be used for static strings or strings that were not explicitly created as a result but existed already prior to even making the call. Commented Nov 1, 2024 at 15:49

1 Answer 1

0

Going by the implementation of ccall(), I would say the memory just leaks if allocated:

var ccall = (ident, returnType, argTypes, args, opts) => {
    var toC = {
        string: str => {
            var ret = 0;
            if (str !== null && str !== undefined && str !== 0) {
                ret = stringToUTF8OnStack(str)
            }
            return ret
        },
        array: arr => {
            var ret = stackAlloc(arr.length);
            writeArrayToMemory(arr, ret);
            return ret
        }
    };

    function convertReturnValue(ret) {
        if (returnType === "string") {
            return UTF8ToString(ret)
        }
        if (returnType === "boolean") return Boolean(ret);
        return ret
    }
    var func = getCFunc(ident);
    var cArgs = [];
    var stack = 0;
    assert(returnType !== "array", 'Return type should not be "array".');
    if (args) {
        for (var i = 0; i < args.length; i++) {
            var converter = toC[argTypes[i]];
            if (converter) {
                if (stack === 0) stack = stackSave();
                cArgs[i] = converter(args[i])
            } else {
                cArgs[i] = args[i]
            }
        }
    }
    var ret = func(...cArgs);

    function onDone(ret) {
        if (stack !== 0) stackRestore(stack);
        return convertReturnValue(ret)
    }
    ret = onDone(ret);
    return ret
};

Basically the return value is fed into convertReturnValue() which in case of a string will do that:

if (returnType === "string") {
    return UTF8ToString(ret)
}

UTF8ToString() reads a string from the WASM memory pointed to by ret and convert it into a JS string. The JS string lives in JS memory and is garbage collected, but the original string in the WASM memory stays untouched and thus is never freed.

So I think the only way to correctly handle the helloWorldDynamic() case is as follows:

async function helloWorldStatic( ): Promise<String>
{
    let module = await Module
    let ptr = module.ccall(
        "helloWorldStatic", // name of the C function
        "number",           // return type (number as it is a pointer!)
        [ ],                // argument types
        [ ]                 // arguments
    )
    let string = UTF8ToString(ptr)
    module._free(ptr)
    return string
}

To make that work, you must ensure to build your WASM code with the following option

-s EXPORTED_FUNCTIONS=_free,...

as only then the free() function is available on your module (which is named _free() as a symbol by C naming conventions). You can also export your own functions that way but it's easier to use EMSCRIPTEN_KEEPALIVE for those in your code, yet you cannot use that macro for functions defined in external libraries.

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

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.