emscripten icon indicating copy to clipboard operation
emscripten copied to clipboard

[BUG] Dlclose behaves differently as compared to native counterpart

Open anutosh491 opened this issue 10 months ago • 10 comments

I don't think you can currently expect dlclose followed by dlopen to replace symbols in the global namespace. I think the first dll to provide I symbol will currently win and that symbol will remain for the life of the program.

If that native behaviour is different to this then I think we would consider a patch to make our behaviour match, in which case please open a bug for that.

Originally posted by @sbc100 in #23793

Here's an example testing the above on my macos

  1. main.cpp
#include <iostream>
#include <dlfcn.h>

typedef int (*getXFunc)();

int main() {
    // Load lib1
    void* handle1 = dlopen("./lib1.dylib", RTLD_NOW | RTLD_GLOBAL);
    if (!handle1) {
        std::cerr << "Failed to load lib1: " << dlerror() << std::endl;
        return -1;
    }

    getXFunc getX1 = (getXFunc)dlsym(handle1, "getX");
    if (!getX1) {
        std::cerr << "Failed to get symbol from lib1: " << dlerror() << std::endl;
        return -1;
    }

    std::cout << "Initial x from lib1: " << getX1() << std::endl;

    // Close lib1
    if (dlclose(handle1) != 0) {
        std::cerr << "Failed to close lib1: " << dlerror() << std::endl;
    } else {
        std::cout << "Closed lib1.so" << std::endl;
    }

    // Load lib2
    void* handle2 = dlopen("./lib2.dylib", RTLD_NOW | RTLD_GLOBAL);
    if (!handle2) {
        std::cerr << "Failed to load lib2: " << dlerror() << std::endl;
        return -1;
    }

    getXFunc getX2 = (getXFunc)dlsym(handle2, "getX");
    if (!getX2) {
        std::cerr << "Failed to get symbol from lib2: " << dlerror() << std::endl;
        return -1;
    }

    std::cout << "x after loading lib2: " << getX2() << std::endl;

    dlclose(handle2);

    return 0;
}
  1. lib1.cpp
#include <iostream>

int x = 10;

extern "C" int getX() {
    return x;
}

__attribute__((constructor))
void init() {
    std::cout << "lib1 loaded, x = " << x << std::endl;
}
  1. lib2.cpp
#include <iostream>
#include <dlfcn.h>

extern "C" int getX(); // Forward declaration

// Define a pointer to x
int* x_ptr = nullptr;

// getX function
extern "C" int getX() {
    return *x_ptr;
}

// Constructor runs when lib2 is loaded
__attribute__((constructor))
void init() {
    std::cout << "lib2 loaded, checking for symbol 'x'..." << std::endl;

    // Look for symbol 'x' in global namespace
    void* sym = dlsym(RTLD_DEFAULT, "x");
    if (sym) {
        std::cout << "Symbol 'x' found from another module." << std::endl;
        x_ptr = (int*)sym;
    } else {
        std::cout << "Symbol 'x' NOT found. Defining x = 99 locally." << std::endl;
        static int local_x = 99; // Define our own
        x_ptr = &local_x;
    }
}

Output

(xeus-cpp) anutosh491@Anutoshs-MacBook-Air test_undo % clang++ -fPIC -shared -o lib1.dylib lib1.cpp
(xeus-cpp) anutosh491@Anutoshs-MacBook-Air test_undo % clang++ -fPIC -shared -o lib2.dylib lib2.cpp
(xeus-cpp) anutosh491@Anutoshs-MacBook-Air test_undo % clang++ -o main main.cpp                    
(xeus-cpp) anutosh491@Anutoshs-MacBook-Air test_undo % ./main                                      
lib1 loaded, x = 10
Initial x from lib1: 10
Closed lib1.so
lib2 loaded, checking for symbol 'x'...
Symbol 'x' NOT found. Defining x = 99 locally.
x after loading lib2: 99

So I tried finding the symbol x in lib2 once dlclose is being applied on lib1 and if the symbol is not found I print a local x. Does this mean if we use dlclose that completely removes the presence of x from the global namespace ?

anutosh491 avatar Mar 17 '25 03:03 anutosh491

Issue https://github.com/emscripten-core/emscripten/issues/23793 describes how the emscripten use case behaves differently than the above.

anutosh491 avatar Mar 17 '25 03:03 anutosh491

gentle ping @sbc100

anutosh491 avatar Mar 24 '25 04:03 anutosh491

Sounds like a real bug. Its not too surprising to me that there might be bugs or differences in behaviour around dlclose since I don't imagine its used much in practice.

sbc100 avatar Mar 24 '25 16:03 sbc100

Thanks for your reply.

Yes, I didn't really expect it to work right away but it sits at the core of my use case :(

Can we think of a roadmap/approach to address this ?

anutosh491 avatar Mar 24 '25 16:03 anutosh491

Dynamic linking is a fairly tricky area to work on, especially when combined with multi-threading. I'm not sure when I might get some time to looking to fixing this I'm afraid.

sbc100 avatar Mar 24 '25 17:03 sbc100

Dynamic linking is a fairly tricky area to work on, especially when combined with multi-threading. I'm not sure when I might get some time to looking to fixing this I'm afraid.

No issues. Let me know if I can somehow help !

anutosh491 avatar Mar 25 '25 05:03 anutosh491

Hey @sbc100 ,

Are there any plans to address this anytime soon ? If not .... that's fine :/ But it would be great to have !

Probably the most requested feature for the Jupyter C++ wasm kernel I maintain!

anutosh491 avatar Nov 13 '25 16:11 anutosh491

I can't say this is high priority right now. Mostly because I believe unloading code is still a very niche use case.

sbc100 avatar Nov 13 '25 17:11 sbc100

FWIW, this is critical to a project I am working on as well (a developer tool with code hotreload workflows).

I built a hacked-up implementation which works on top of emscripten's (without needing to modify your emscripten sdk). I don't think it's very clean, it leaks memory, it is likely to break when updating emscripten because it relies on internals, but it does the job for us for now. Would love to have official support instead.

My code:

C-code:

// WARNING: Emscripten internals. May need to update these when switching to a different emscripten version
// which includes a different libc implementation.
//
// We need these, because emscripten does not have a dlclose implementation. To add dlclose functionality,
// we need to unload the wasm module in JS, but we also need to remove the representation of the library from
// the linked list maintained by the libc implementation of dlopen (otherwise we just get the same handle again
// when trying to load a library a second time). For this we need to know the internal data structure of that
// linked list.
//
// Snippet from emscripten/system/lib/libc/musl/src/internal/dynlink.h

struct dso {
    // Pointer back to the dlevent in the event sequence which loaded this DSO.
    struct dlevent* event;

    // Flags used to open the library.  We need to cache these so that other
    // threads can mirror the open library state.
    int flags;

    // Location in memory/table of static data/static function addresses
    // The first thread to load a given module alloces the memory and table
    // address space and then sets this field to non-zero.
    uint8_t mem_allocated;
    void* mem_addr;
    size_t mem_size;
    void* table_addr;
    size_t table_size;

    // For DSO load events, where the DSO comes from a file on disc, this
    // is a pointer the file data read in by the laoding thread and shared with
    // others.
    uint8_t* file_data;
    size_t file_data_size;

    // Flexible array; must be final element of struct
    char name[];
};

// Snippet from emscripten/system/lib/libc/dynlink.c

struct dlevent {
    struct dlevent *next, *prev;
    // Symbol index resulting from dlsym call. -1 means this is a dso event.
    int sym_index;
    // dso handler resulting fomr dleopn call.  Only valid when sym_index is -1.
    void* dso;
};


// This is our own dlclose js implementation, implemented in ldso.js
extern "C" void curiosity_js_dlclose(void* handle);

// WARNING: Emscripten internals.
// We remove the library to be unloaded from the libc linked list by linking the previous and next entries to each other.
// This assures that future attempts to load the library will not resolve to the old handle.
// TODO: in doing so, we leak the memory representation in the linked list. See if we can and should fix this.
void curiosity_c_dlclose(void *h)
{
    dso* dso = (struct dso*)h;
    dlevent* event = dso->event;
    event->prev->next = event->next;
    event->next->prev = event->prev;
}

void dlclose(void* lib)
{
    curiosity_js_dlclose(lib);
    curiosity_c_dlclose(lib);
}

js-code

	curiosity_js_dlclose: function(handle) {
		if (!LDSO || !LDSO.loadedLibsByHandle) {
			console.error("LDSO not initialized");
			return;
		}

		var lib = LDSO.loadedLibsByHandle[handle];
		if (!lib) {
			console.error("No loaded module for handle:", handle);
			return;
		}

		// Remove all global symbols contributed by this module
		for (var sym in lib.exports) {
			delete wasmImports[sym];
			delete GOT[sym];
		}

		// Remove from loadedLibs* tables
		delete LDSO.loadedLibsByName[lib.name];
		delete LDSO.loadedLibsByHandle[handle];

		lib.module = null;
		lib.global = null;
		lib.exports = null;
	},

jechter avatar Nov 14 '25 09:11 jechter

Thank you @jechter

The above looks interesting.

anutosh491 avatar Nov 14 '25 09:11 anutosh491