[education/rkward] rkward/rbackend: Turns out neither RUNPATH nor RPATH will help us (see details added in rkapi.h).

Thomas Friedrichsmeier null at kde.org
Fri May 10 17:04:24 BST 2024


Git commit dd83fa4ca15d7daa2ffd8eb4f18c0b88408bea45 by Thomas Friedrichsmeier.
Committed on 09/05/2024 at 21:13.
Pushed by tfry into branch 'master'.

Turns out neither RUNPATH nor RPATH will help us (see details added in rkapi.h).

Use a different trick, instead.

M  +10   -0    rkward/rbackend/rkfrontendtransmitter.cpp
M  +22   -12   rkward/rbackend/rkrapi.h
M  +64   -19   rkward/rbackend/rkrbackend_dlopen.cpp
M  +1    -1    rkward/rbackend/rkrbackendprotocol_backend.cpp

https://invent.kde.org/education/rkward/-/commit/dd83fa4ca15d7daa2ffd8eb4f18c0b88408bea45

diff --git a/rkward/rbackend/rkfrontendtransmitter.cpp b/rkward/rbackend/rkfrontendtransmitter.cpp
index 5767eeb2d..b00f67a9b 100644
--- a/rkward/rbackend/rkfrontendtransmitter.cpp
+++ b/rkward/rbackend/rkfrontendtransmitter.cpp
@@ -23,6 +23,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
 #include <QDir>
 #include <QStandardPaths>
 #include <QElapsedTimer>
+#include <QTemporaryDir>
 
 #include "../version.h"
 #include "../debug.h"
@@ -158,11 +159,20 @@ void RKFrontendTransmitter::run () {
 	}
 
 #if defined(RK_DLOPEN_LIBRSO)
+	/** NOTE: For a description of the rationale for this involved loading procedure rkapi.h ! */
 	QString backend_lib = findBackendLibAtPath(QCoreApplication::applicationDirPath()); // for running directly from the build tree, but also covers windows
 	if (backend_lib.isEmpty()) backend_lib = findBackendLibAtPath(QCoreApplication::applicationDirPath() + "/../lib"); // covers rkward in /usr[/local]/bin and lib in /usr/[/local]/lib
 	                      // but also backend in /usr/lib/libexec and lib in /usr/lib-> regular install on Linux
 	if (backend_lib.isEmpty()) backend_lib = findBackendLibAtPath(QFileInfo(backend_executable).absolutePath()); // backend and lib both installed in libexec or similar
+#	if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
 	env.append(QStringLiteral("RK_BACKEND_LIB=") + backend_lib);
+#	else
+	env.append(QStringLiteral("RK_BACKEND_LIB=") + QFileInfo(backend_lib).fileName());
+	QTemporaryDir rkward_only_dir("rkward_only");
+	QFile(QFileInfo(backend_lib).absolutePath()).link(rkward_only_dir.filePath("_rkward_only_dlpath"));
+	env.append(QStringLiteral("RK_ADD_LDPATH=./_rkward_only_dlpath"));
+	env.append(QStringLiteral("RK_LD_CWD=") + rkward_only_dir.path());
+#	endif
 #endif
 	backend->setEnvironment(env);
 
diff --git a/rkward/rbackend/rkrapi.h b/rkward/rbackend/rkrapi.h
index 0091f0cd4..fb4d26ed3 100644
--- a/rkward/rbackend/rkrapi.h
+++ b/rkward/rbackend/rkrapi.h
@@ -21,23 +21,33 @@ Problem description:
     Here, there is no clash of conflicting libraries, but a clash with two users of the library accessing the same singletons invalidly.
 3. The problem is aggravated by the fact that R may load essentially abitrary further libraries from packages. In the 2a) case, we want to load
    all those from system locations, while the AppImage location is only for the Qt functions in rkward.rbackend.
+4. R itself (time of writing: R 4.4, but unchanged in this respect since ages) tries to ensure correct lib search path by setting LD_LIBRARY_PATH to:
+   /Rs/own/libdir:/Rs/idea/of/where/system/libs/might/be/at:/Rs/java/libdir:/anything/previously/in/LD_LIBRARY_PATH
+   Now this puts several directories in front of the search path which may well contain libraries conflicting with the Qt libs we need.
+   E.g. it may contain /usr/lib/, while we'd hope to load Qt libs from inside an Appimage.
+5. Note that LD_LIBRARY_PATH overrides RUNPATH. RPATH is deprecated, and while it will probably be around for a long time to come, it is not so easy
+   to set. In fact, linuxdeployqt (the tool used to create Appimages in KDE infastructure) foolishly overwrites any RPATH we may set, manually.
 
-So how do we get two sets of libraries to load from two different search paths, without getting into the way of each other? Keeping them compartmentalised is
+So how do we get two sets of libraries to load from two different search paths, without getting into the way of each other? Keeping them compartimentalised is
 done by loading them using dlopen() with the RTLD_LOCAL flag set. Now, dlopen() is "easy" enough for dealing with a plain C library like libR.so. (That's essentially
-what is done in this file).
+what is done in this file). We shove everything else into another shared lib (librkward.rbackend.lib.so), so we can similarily dlopen it with RTLD_LOCAL. This lib
+exports a single (C) entry-point ("do_main()"), and can simply be handed a pointer to the libR.so, from where to resolve all required symbols.
 
-As for loading the two from different search paths, this problem cannot easily be resolved using either LD_LIBRARY_PATH (changes of which do not take
-effect, if they happen during the lifetime of the executable). BTW, it should be noted that R CMD itself configures LD_LIBRARY_PATH.
-RPATH or RUNPATH also cannot selectively pick one set of libraries from one search path, and another from another - at least not in a single library file.
-What can be done, however, is the following setup:
+As for loading the two from different search paths, having already compartimentalised our two sets of libs, we somehow need to make sure, the two sets are loaded
+with different effective search paths. RUNPATH and even RPATH won't help us here (see above), and changes done to LD_LIBRARY_PATH do not take
+effect during the lifetime of an executable.
 
-1. Frontend sets up environment, removing any LD_LIBRARY_PATH pointing to inside the AppImage. Calls rkward.rbackend
+1. Frontend creates a temporary dir with a symlink pointing to the path of the qt libs we want to load
+2. Frontend sets up environment, removing any LD_LIBRARY_PATH pointing to inside the AppImage, and sets some further envs vars (see below). Calls rkward.rbackend
 2. rkward.rbackend is just a tiny binary that dlopen()s two further things (each with RTLD_LOCAL):
-3. libR.so - this is easy enough to get from the right place, as it is not in any default path
-4. rkward.rbackend.lib encapsulates all Qt calls, and links against Qt libs (and dependencies), only. These are C++ and thus hard to load, but we
-   only need a single entry point from rkward.rbackend: "do_main(argc, argv)", and hand it the handle of libR.so, so it can look up the required R
-   symbols. (Note that is does not _load_ libR.so, only resolves the symbols.)
-   rkward.rbackend.lib should have RPATH/RUNPATH set when in an AppImage, pointing to the libs in the image.
+3. libR.so - this is easy enough to get from the right place, as it is not in any default path, and R CMD has added the appropriate stuff to LD_LIBRARY_PATH
+4. For loading librkward.rbackend.lib.so, we need a more complex approach:
+4a. First, we prepend the name of the symlink from step 1 to LD_LIBRARY_PATH as a *relative* path (relative to the temporary dir).
+4b. We execv() rkward.rbackend, again, for this to take effect.
+4c. We cd to the temporary dir: From this dir - and *only* from this dir - our addition to LD_LIBRARY_PATH will point to our desired path of qt libs
+4d. We dlopen() librkward.rbackend.lib.so
+4e. We cd back to where we were before 4c.
+5. do_main() is resolved from librkward.rbackend.lib.so, and called with a handle to libR.so to resolve all required R symbols.
 
 ---
 
diff --git a/rkward/rbackend/rkrbackend_dlopen.cpp b/rkward/rbackend/rkrbackend_dlopen.cpp
index fce369249..c135dd1ef 100644
--- a/rkward/rbackend/rkrbackend_dlopen.cpp
+++ b/rkward/rbackend/rkrbackend_dlopen.cpp
@@ -14,9 +14,13 @@ SPDX-License-Identifier: GPL-2.0-or-later
 #ifdef Win32
 #include <windows.h>
 #else
+#include <unistd.h>
+#include <string>
 #include <dlfcn.h>
 #endif
 
+#include <filesystem>
+
 void *resolve_symb(void* dllinfo, const char* name) {
 #ifdef Win32
 	return GetProcAddress((HMODULE) dllinfo, name);
@@ -25,37 +29,78 @@ void *resolve_symb(void* dllinfo, const char* name) {
 #endif
 }
 
+auto loadlib(const char* name) {
+#ifdef Win32
+	auto ret = LoadLibraryA(name);
+#else
+	auto ret = dlopen(name, RTLD_NOW | RTLD_LOCAL); // NOTE: RTLD_DEEPBIND causes undiagnosed runtime failure on Suse Tumbleweed around 05/24 (while it works, elsewhere)
+#endif
+	if (!ret) {
+#ifdef Win32
+		fprintf(stderr, "Failure to open lib %s\n", name);
+#else
+		fprintf(stderr, "Failure to open lib %s: %s\n", name, dlerror());
+#endif
+		exit(99);
+	}
+	return ret;
+}
+
 int main(int argc, char *argv[]) {
 // TODO: Take lib name from CMake?
 // TODO: Use dlmopen, where available?
-	const char* backendlib = getenv("RK_BACKEND_LIB");
-	if (!backendlib) {
-		fprintf(stderr, "Backend lib not specified!");
+
+/** NOTE: For a description of the rationale for this involved loading procedure rkapi.h ! */
+	if (argc > 10) {
+		fprintf(stderr, "Too many args\n"); // and I'm lazy
 		exit(99);
 	}
-#ifdef Win32
-	auto r_dllinfo = LoadLibraryA("R.dll");
-	auto rkb_dllinfo = LoadLibraryA(backendlib);
-#else
-#	ifdef __APPLE__
-	auto r_dllinfo = dlopen("libR.dylib", RTLD_NOW | RTLD_LOCAL);
-#	else
-	auto r_dllinfo = dlopen("libR.so", RTLD_NOW | RTLD_LOCAL);  // NOTE: RTLD_DEEPBIND causes undiagnosed runtime failure on Suse Tumbleweed around 05/24 (while it works, elsewhere)
-#	endif
-	auto rkb_dllinfo = dlopen(backendlib, RTLD_NOW | RTLD_LOCAL);
+#ifndef Win32
+	// Prepend out own load path to what R CMD has set
+	// Note that we're using a relative path that will only find something if starting from the correct dir
+	const char RK_ADD_LDPATH[] = "RK_ADD_LDPATH";
+	const char LD_LIBRARY_PATH[] = "LD_LIBRARY_PATH";
+	const char* addldpath = getenv(RK_ADD_LDPATH);
+	if (addldpath && addldpath[0]) {
+		std::string curldpath = addldpath;
+		char* c_curldpath = getenv(LD_LIBRARY_PATH);
+		if (c_curldpath && c_curldpath[0]) {
+			curldpath += ":";
+			curldpath += c_curldpath;
+		}
+		setenv(LD_LIBRARY_PATH, curldpath.c_str(), 1);
+		unsetenv(RK_ADD_LDPATH);
+		char* const args[] = { argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9], NULL };
+		execv(argv[0], args);
+	}
 #endif
-	if (!rkb_dllinfo) {
-		fprintf(stderr, "Failure to open backend lib from %s", backendlib);
+
+	const char* backendlib = getenv("RK_BACKEND_LIB");
+	if (!backendlib || !(backendlib[0])) {
+		fprintf(stderr, "Backend lib not specified!\n");
 		exit(99);
 	}
-	if (!r_dllinfo) {
-		fprintf(stderr, "Failure to open R lib");
-		exit(99);
+
+#if defined(Win32)
+	auto r_dllinfo = loadlib("R.dll");
+#elif defined(__APPLE__)
+	auto r_dllinfo = loadlib("libR.dylib");
+#else
+	auto r_dllinfo = loadlib("libR.so");
+#endif
+
+	char* c_rk_ld_cwd = getenv("RK_LD_CWD");
+	auto cd = std::filesystem::current_path();
+	if (c_rk_ld_cwd && c_rk_ld_cwd[0]) {
+		std::filesystem::current_path(c_rk_ld_cwd);
 	}
+	auto rkb_dllinfo = loadlib(backendlib);
+	std::filesystem::current_path(cd);
+
 	int (*do_main) (int, char**, void*, void* (*)(void*, const char*));
 	do_main = (decltype(do_main)) resolve_symb(rkb_dllinfo, "do_main");
 	if (!do_main) {
-		fprintf(stderr, "Failure to resolve do_main()");
+		fprintf(stderr, "Failure to resolve do_main(): %s\n", dlerror());
 		exit(99);
 	}
 	return do_main(argc, argv, r_dllinfo, resolve_symb);
diff --git a/rkward/rbackend/rkrbackendprotocol_backend.cpp b/rkward/rbackend/rkrbackendprotocol_backend.cpp
index 9f80d4bc6..b34888fd8 100644
--- a/rkward/rbackend/rkrbackendprotocol_backend.cpp
+++ b/rkward/rbackend/rkrbackendprotocol_backend.cpp
@@ -127,7 +127,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
 #endif
 
 		RKRBackendTransmitter transmitter (servername, token);
-		RKRBackendProtocolBackend::p_transmitter = &transmitter; // cppcheck-suppress danlingReference  -> valid for the lifetime of the backend
+		RKRBackendProtocolBackend::p_transmitter = &transmitter; // cppcheck-suppress danlingReference ; -> valid for the lifetime of the backend
 		RKRBackendProtocolBackend backend (data_dir, rkd_server_name);
 		transmitter.start ();
 		RKRBackend::this_pointer->run (locale_dir);



More information about the rkward-tracker mailing list