Issue
This question is directly related to this one; I've been advised to not mix many questions into one, so I'm posting individual questions around this topic.
I am working on a project whose source is built for both Linux and Windows. The project was originally built with handwritten Makefiles, because of which shared libraries are built with .so
extensions both on Linux and Windows. After migrating the project to a CMake
-based build, shared libraries are build with .so
extensions on Linux, and .dll
extensions on Windows. Because of this, the application fails at runtime because it has been hardcoded to lazy-load (dlopen()
) shared libraries with .so
extensions.
I believe that the differences between dynamic link libraries and shared libraries are not relevant to the immediate need because if I rename the generated .dll
files to .so
, the application runs to completion successfully.
My question is: is there standard or best-practice way to deal with the shared library filename extension difference between Linux and Windows? I can imagine a macro-based solution, e.g.:
#ifdef SOMETHING
#define EXTENSION ".so"
#else
#define EXTENSION ".dll"
#endif
...
void* handle = dlopen("libfunc" EXTENSION, RTLD_NOW)
...but I'd like to learn if there is a widely-adopted best practice or prevailing convention, or outright correct way to handle this. In essence, the need is "when in Windows, load files with .dll
extension; when in Linux, load files with .so
extension."
(I am assuming that handwritten Makefile
that created .so
files for all platforms did so out of inexperience or convenience, etc. and that CMake
is doing the correct or preferred thing by generating .so
files in Linux and .dll
files in Windows. I.e. I'm assuming that the onus is on the developer to accommodate compile-time or runtime conditionality on the extension of shared libraries he loads with dlopen()
in his code)
Below are resources for a minimal, compilable example:
// main.c
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char* argv[]) {
int (*fptr)(void) = NULL;
const char* filename = "libfunc.so"; // Nota bene
void* handle = dlopen(filename, RTLD_NOW);
if (! handle) {
fprintf(stderr, "%s\n", dlerror());
exit(EXIT_FAILURE);
}
fptr = dlsym(handle, "func");
fprintf(stdout, "fptr returns %d\n", (*fptr)());
dlclose(handle);
return 0;
}
// dlfcn.h
#ifndef DLFCN_H
#define DLFCN_H
#include <windows.h>
#define RTLD_NOW (1<<0)
extern inline void *dlopen(const char *lib, int flags) {
(void) flags;
return LoadLibraryExA(lib, NULL, 0);
}
extern inline void *dlsym(void *handle, const char *name) {
FARPROC fp = GetProcAddress((HINSTANCE) handle, name);
return (void *)(intptr_t)fp;
}
extern inline char *dlerror() {
// Not thread-safe!
static char msg[1024];
msg[0] = '\0';
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, GetLastError(), 0, msg, sizeof(msg), NULL);
return msg;
}
extern inline int dlclose(void *handle) {
return FreeLibrary(handle) ? 0 : -1;
}
// func.c
int func(void) {
return 42;
}
# Makefile
.PHONY: all
CFLAGS :=
ifeq ($(shell uname), Windows_NT)
CFLAGS += -isystem .
endif
all: libfunc.so a.out
libfunc.so: func.o
cc -shared -fpic -o $@ $^
a.out: libfunc.so main.c
cc -o $@ main.c ${CFLAGS}
# CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
project(hello_world)
add_library(func SHARED func.c)
add_executable(a.out main.c)
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
include_directories(SYSTEM ${CMAKE_CURRENT_SOURCE_DIR})
endif()
Compiling with make
and running on Linux:
$ make
cc -c -o func.o func.c
cc -shared -fpic -o libfunc.so func.o
cc -o a.out main.c
$ LD_LIBRARY_PATH=$LD_LIBRARY_PATH:. ./a.out
fptr returns 42
$
Compiling with cmake
and running on Linux:
$ cmake -B build && cmake --build build
-- The C compiler identification is GNU 11.4.0
-- The CXX compiler identification is GNU 11.4.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/user/dev/shared-lib-test/build
[ 25%] Building C object CMakeFiles/func.dir/func.c.o
[ 50%] Linking C shared library libfunc.so
[ 50%] Built target func
[ 75%] Building C object CMakeFiles/a.out.dir/main.c.o
[100%] Linking C executable a.out
[100%] Built target a.out
$
$ LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD/build ./build/a.out
fptr returns 42
$
Compiling with make
and running in w64devkit:
$ make
cc -isystem . -c -o func.o func.c
cc -shared -fpic -o libfunc.so func.o
cc -o a.out main.c -isystem .
$
$ ./a.out
fptr returns 42
$
Compiling with cmake
and running in w64devkit:
$ cmake -G "Unix Makefiles" -B build && cmake --build build
-- The C compiler identification is GNU 13.2.0
-- The CXX compiler identification is GNU 13.2.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: C:/Program Files/w64devkit/bin/cc.exe - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: C:/Program Files/w64devkit/bin/c++.exe - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (3.1s)
-- Generating done (0.1s)
-- Build files have been written to: C:/Users/user/dev/shared-lib-test/build
[ 25%] Building C object CMakeFiles/func.dir/func.c.obj
[ 50%] Linking C shared library libfunc.dll
[ 50%] Built target func
[ 75%] Building C object CMakeFiles/a.out.dir/main.c.obj
[100%] Linking C executable a.out.exe
[100%] Built target a.out
$
$ ./build/a.out.exe
The specified module could not be found.
$ mv ./build/libfunc.dll ./build/libfunc.so
$
$ ./build/a.out.exe
fptr returns 42
$
Solution
is there standard or best-practice way to deal with the shared library filename extension difference between Linux and Windows?
Your code to load the library is already very different -- the Windows version uses LoadLibraryExA
, while the UNIX version uses dlopen
.
The standard/best practice way to handle platform differences is not to emulate dlopen
on Windows (as your code does), but instead to provide OS-specific load_library
function (in different source files).
That is, most of your application doesn't care about the differences, and just calls load_library("func");
.
The unix.c
implements load_library()
as (overflow checking omitted for clarity):
void *load_library(const char *name)
{
char buf[1024] = "lib";
strcpy(buf + 3, name);
strcat(buf, ".so");
return dlopen(buf, ...); // dlopen("lib{name}.so")
}
while windows.c
implements it as:
void *load_library(const char *name)
{
char buf[1024];
strcpy(buf, name);
strcat(buf, ".dll");
return LoadLibraryExA(buf, NULL, 0); // LoadLibraryExA("{name}.dll")
}
You then have a selector in the Makefile
which uses unix.c
or windows.c
as appropriate.
Answered By - Employed Russian Answer Checked By - Willingham (WPSolving Volunteer)