I can't get a minimal reproduceable example any smaller than what I have. Mainly because I can't debug the issue, it seems completely random and I don't consistently get the error, sometimes it compiles, sometimes it doesn't. I have reverted back to when I had the error so I can copy the code, but since then, I haven't gotten an error. Basically, I have no other ideas left other than to fix all my linker warnings. I remember it was mostly related to this warning LNK4042 object specified more than once; extras ignored in main.obj. There is also another warning I don't understand, that being I have the binaries for a library named GLEW, yet statically linking it invokes a linker warning, LNK4099 PDB 'vc120.pdb' was not found with 'glew32s.lib(glew.obj)' or at '\SolutionDir\Debug\vc120.pdb'; linking object as if no debug info
What are the fixes for these warnings? Hopefully to stop me randomly getting errors that, when I change one thing it compiles, and then revert it back and it still compiles.
Here is the code, main.h
#pragma once
#ifndef MAIN_H
#define MAIN_H
#include <GL/glew.h> // Initialize with glewInit()
#ifndef GLFW_INCLUDE_NONE
#define GLFW_INCLUDE_NONE // GLFW including OpenGL headers causes ambiguity or multiple definition errors.
#endif // GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>
#include "ImGui/imgui.h"
#include "ImGui/implot.h"
#include "ImGui/imgui_impl_glfw.h"
#include "ImGui/imgui_impl_opengl3.h"
#include <iostream> //std::string
class Main ///Singleton
{
public:
Main(const Main&) = delete;
Main(Main&&) = delete;
Main& operator=(const Main&) = delete;
Main& operator=(Main&&) = delete;
private:
Main();
static Main& Get_Instance();
friend int main(int argc, char* argv[]);
static void Mainloop();
static void Init();
static void Free();
GLFWwindow* m_Window = nullptr;
std::string m_GLSL_Version = "";
int m_Window_Width = 1280;
int m_Window_Height = 720;
};
#endif // MAIN_H
and main.cpp
#include "main.h"
Main::Main()
{
}
Main& Main::Get_Instance()
{
static Main instance;
return instance;
}
void Main::Init()
{
// Setup window
Get_Instance(); //Init constructor
if (!glfwInit()) {
std::cout << "Could not initialize GLFW" << std::endl;
std::cin.get();
exit(EXIT_FAILURE);
}
Get_Instance().m_GLSL_Version = "#version 460";
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
// Create window with graphics context
Get_Instance().m_Window = glfwCreateWindow(Get_Instance().m_Window_Width, Get_Instance().m_Window_Height, "Program", NULL, NULL);
if (Get_Instance().m_Window == NULL) {
std::cout << "Could not create GLFW window" << std::endl;
std::cin.get();
exit(EXIT_FAILURE);
}
glfwMakeContextCurrent(Get_Instance().m_Window);
glfwSwapInterval(0); // vsync
if (glewInit() != GLEW_OK)
{
fprintf(stderr, "Failed to initialize OpenGL loader!\n");
std::cin.get();
exit(EXIT_FAILURE);
}
// Setup Dear ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImPlot::CreateContext();
// Setup Dear ImGui style
ImGui::StyleColorsClassic();
// Setup Platform/Renderer backends
ImGui_ImplGlfw_InitForOpenGL(Get_Instance().m_Window, true);
ImGui_ImplOpenGL3_Init(Get_Instance().m_GLSL_Version.c_str());
}
void Main::Mainloop()
{
// Main loop
while (!glfwWindowShouldClose(Get_Instance().m_Window))
{
//Events
glfwPollEvents();
// Start the Dear ImGui frame
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
ImGui::Text("%.3f", ImGui::GetIO().Framerate);
// Rendering
ImGui::Render();
glfwGetFramebufferSize(Get_Instance().m_Window, &Get_Instance().m_Window_Width, &Get_Instance().m_Window_Height);
glViewport(0, 0, Get_Instance().m_Window_Width, Get_Instance().m_Window_Height);
glClearColor(0.45f, 0.55f, 0.60f, 1.00f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glfwSwapBuffers(Get_Instance().m_Window);
}
}
void Main::Free()
{
// Cleanup
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImPlot::DestroyContext();
ImGui::DestroyContext();
glfwDestroyWindow(Get_Instance().m_Window);
glfwTerminate();
}
int main(int argc, char* argv[])
{
Main::Init();
Main::Mainloop();
Main::Free();
return 0;
}
Edit: It happened again, here are the errors:
Error LNK1561 entry point must be defined
Warning LNK4042 object specified more than once; extras ignored (THIS IS IN MY main.obj file, its the only linker issue that references my actual files, the rest reference either 'LINK' or 'libucrt.lib(something here)'
Error LNK2005 __cexit already defined in ucrtd.lib(ucrtbased.dll)
Error LNK2005 __crt_atexit already defined in ucrtd.lib(ucrtbased.dll)
Error LNK2005 __crt_at_quick_exit already defined in ucrtd.lib(ucrtbased.dll)
Error LNK2005 __execute_onexit_table already defined in ucrtd.lib(ucrtbased.dll)
Error LNK2005 __initialize_narrow_environment already defined in ucrtd.lib(ucrtbased.dll)
Error LNK2005 __initialize_onexit_table already defined in ucrtd.lib(ucrtbased.dll)
Error LNK2005 __register_onexit_function already defined in ucrtd.lib(ucrtbased.dll)
Error LNK2005 __seh_filter_dll already defined in ucrtd.lib(ucrtbased.dll)
Regarding the LNK4042 warning, you may be able to resolve the issue, if you open the properties for the entire project, and the change the value under C/C++ -> Output Files -> "Object File Name" to be the following:
$(IntDir)/%(RelativeDir)/
Related
I'm writing a program (macOS, clang++ compiler, only AppleSilicon at the moment) that I can extend later by providing custom plugins (dynamic library, loaded at runtime) which use main program's public interface.
test.hpp - public interface:
#if defined(MAGIC_PLUGIN)
# define MAGIC_EXPORT /* nothing */
#else
# define MAGIC_EXPORT __attribute__((visibility("default")))
#endif
MAGIC_EXPORT
void testCall();
test.cpp - main programm:
#include <stdio.h>
#include <dlfcn.h>
#include "test.hpp"
// Declare a function to call from a loaded plugin
typedef void (* plugin_call_func)(void);
int main(int argc, char** argv) {
// Load library
const char* libraryName = "plugin.dylib";
void* library = dlopen(libraryName, RTLD_NOW);
if (library == nullptr) {
printf("Cannot open library\n");
return 1;
}
// Get function from loaded library
plugin_call_func pluginCall = reinterpret_cast<plugin_call_func>(
dlsym(library, "pluginCall"));
if (pluginCall == nullptr) {
printf("Cannot find the pluginCall function\n");
return 2;
}
// Execute loaded function
pluginCall();
// Forget function and close library
pluginCall = nullptr;
auto libraryCloseResult = dlclose(library);
if (libraryCloseResult != 0) {
printf("Cannot close library\n");
return 3;
}
return 0;
}
// Public function, should be called from a plugin
void testCall() {
printf("Test call\n");
}
plugin.cpp - plugin's source:
#define MAGIC_PLUGIN
#include <stdio.h>
#include "test.hpp"
__attribute__((visibility("default")))
extern "C" void pluginCall(void) {
printf("Plugin call\n");
testCall();
}
First, I compile main app:
clang++ -std=c++20 -fvisibility=hidden -target arm64-apple-macos12 test.cpp -o test
The nm --defined-only test shows these symbols:
0000000100003ee4 T __Z8testCallv
0000000100000000 T __mh_execute_header
0000000100003dcc t _main
Mangled __Z8testCallv is what I need. Everything looks good so far. But then I try to compile the plugin as dynamic library...
clang++ -std=c++20 -fvisibility=hidden -dynamiclib -g -current_version 0.1 -target arm64-apple-macos12 plugin.cpp -o plugin.dylib
and get this error:
Undefined symbols for architecture arm64:
"testCall()", referenced from:
_pluginCall in plugin-38422c.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
Well, it's kind of fair, I can understand this, because the dynamic library does not know that testCall is somewhere implemented. So I want to say it that it does not have to worry about testCall's existence.
I tried to research how to do this, looked up man pages, read tons of stack overflow answers, and what I only found that works is adding these flags to linker:
-Wl,-undefined,dynamic_lookup
It works, the library compiles and the app works as expected. But I don't really want to use dynamic_lookup because it will mark every undefined symbol in the library as resolved, which may lead to some bad consequences. I want to tell the linker only about existence of the main program's public symbols.
What am I missing? Is there any better solution than dynamic_lookup?
Your best bet is to manually do the work that's done by the library loader. That is: populating function pointers. After all, the plugin->main binding is already done manually, so doing the same thing the other way around makes sense.
You can make this process essentially transparent by carefully crafting the header shared by the plugin and main application. The only tricky part is handling ODR for plugins that are composed of multiple source files.
Since this is a C++ question, here's what it could look like with a RAII wrapper. The ODR conundrum is handled via the PLUGIN_MAIN macro that should only be defined in one of a plugin's sources.
test_plugin.hpp
using pluginCall_fn = void(*)();
using testCall_fn = void(*)();
#if !defined(MAIN_APPLICATION)
#if defined(PLUGIN_MAIN)
#define EXPORTED_FROM_MAIN __attribute__((visibility("default")))
#else
#define EXPORTED_FROM_MAIN __attribute__((visibility("default"))) extern
#endif
extern "C" {
// Declare symbols provided by the plugin
__attribute__((visibility("default"))) void pluginCall();
// etc...
// Declare/define pointers that will be populated by the main application
EXPORTED_FROM_MAIN testCall_fn testCall;
// etc...
}
#undef EXPORTED_FROM_MAIN
#else // In the main app.
#include <stdexcept>
// Declare "symbols" provided by the main application
void testCall();
// Utility class to load/unload a dynamic library.
// Probably belongs in its own header...
struct loaded_library final {
loaded_library(const char* libName)
: handle_(dlopen(libName, RTLD_NOW)) {
if(!handle_) {
throw std::runtime_error("failed to load plugin");
}
}
loaded_library(const loaded_library&) = delete;
loaded_library& operator=(const loaded_library&) = delete;
loaded_library(loaded_library&& rhs) : handle_(rhs.handle_) {
rhs.handle_ = nullptr;
}
loaded_library& operator=(loaded_library&& rhs) {
handle_ = rhs.handle_;
rhs.handle_ = nullptr;
return *this;
}
~loaded_library() {
if(handle_) {
dlclose(handle_);
}
}
template<typename T>
T get_symbol(const char* symbol) {
T result = reinterpret_cast<T>(dlsym(handle_, symbol));
if(!result) {
throw std::runtime_error("missing symbol");
}
return result;
}
private:
void* handle_;
};
// Plugin interface.
struct loaded_plugin final {
loaded_plugin(const char* libName)
: lib_(libName) {
// Load functions from plugin
pluginCall = lib_.get_symbol<pluginCall_fn>("pluginCall");
// ...
// Assign callbacks to plugin
*lib_.get_symbol<testCall_fn*>("testCall") = &testCall;
// ...
// Call the plugin's init function here if applicable.
}
pluginCall_fn pluginCall;
private:
loaded_library lib_;
};
#endif
plugin.cpp
#define PLUGIN_MAIN
#include "test_plugin.hpp"
#include <stdio.h>
void pluginCall() {
printf("Plugin call\n");
testCall();
}
test.cpp
#define MAIN_APPLICATION
#include "test_plugin.hpp"
int main(int argc, char** argv) {
const char* libraryName = "plugin.dylib";
loaded_plugin plugin(libraryName);
plugin.pluginCall();
}
// Public function, should be called from a plugin
void testCall() {
printf("Test call\n");
}
You may find that this code is a bit on the fragile side of things, since a few different portions of test_plugin.hpp need to be kept in sync.
This can be worked around with the use of X-Macros, at the cost of confusing IDEs and hurting code legibility. I wouldn't go down that road until the APIs in question become unwieldingly large.
Is there any reason why this code here:
int main(int argc, char* argv[])
{
Main::Init();
std::thread worker(Main::Mainloop);
worker.join();
Main::Free();
return 0;
}
should work differently to this code here:
int main(int argc, char* argv[])
{
Main::Init();
Main::Mainloop();
Main::Free();
return 0;
}
Noting that the Main class is defined as a singleton, here is the code:
main.h
#pragma once
#ifndef MAIN_H
#define MAIN_H
#include "window.h"
#include "mainloop.h"
class Main ///Singleton
{
public:
Main(const Main&) = delete;
Main(Main&&) = delete;
Main& operator=(const Main&) = delete;
Main& operator=(Main&&) = delete;
private:
Main();
static Main& Get_Instance();
friend int main(int argc, char* argv[]);
static void Mainloop();
static void Init();
static void Free();
};
#endif // MAIN_H
The first example above fails to initialise one ofGLFW, GLEW, and ImGui which are what I am using for my program. I was trying to split up the initialisation of the program but then i ran into this issue. When I dug farther I reached this point which doesn't really make any sense why it shouldn't work. Basically it either throws an exception or ImGui spams me with many errors during runtime saying:
failed to compile vertex shader!
failed to compile fragment shader!
failed to link shader program! (with GLSL `#version 460`)
yet the window opens up and I only get these during runtime with the thread example. Not with the other one.
All of those libraries in some way interact with OpenGL and therefore are very sensitive to what thread they're being executed on. The current OpenGL context is thread-specific; each thread has its own current context, and a context can only be current within one thread at any time.
Creating a GLFW window creates an OpenGL context. If you then switch to another thread, that context will not be current in that thread unless you tell GLFW to make it current in that thread.
I have the code: loading* loadingScreen = new loading(device);
When I compile the program, the compiler complains that loading screen wasn't declared:
error: 'loadingScreen' was not declared in this scope
loading* loadingScreen = new loading(device);
^
I'm unsure what's causing this. What I've tried:
Rewriting it so that the constructor isn't run.
Relocating the declaration to a different section of code.
Making sure my classes are included correctly.
Here's my main.cpp file:
#include <iostream>
#include <irrlicht.h>
#include <future> // std::async, std::future
#include <chrono> // Millisecond timer
#include <pthread.h>
#include <loading.h>
#include <fps.h>
bool loading = false;
struct load_struct {
irr::IrrlichtDevice *device;
fps *fpsLevel;
};
void load(void * loadingStruct)
{
loading = true;
struct load_struct *args = (struct load_struct *) loadingStruct;
loading = false;
pthread_exit(NULL);
}
int main()
{
//Create display to optimal settings.
irr::IrrlichtDevice *device = irr::createDevice(irr::video::EDT_NULL);
irr::s32 depth = device->getVideoModeList()->getDesktopDepth();
irr::core::dimension2d<irr::u32> resolution = device->getVideoModeList()->getDesktopResolution();
device->drop();
device = irr::createDevice(irr::video::EDT_OPENGL, resolution, depth, true, true, true, NULL); // TODO: Change last parameter
if(device==NULL)
{
std::cout << "Unable to create device! Do you have graphics drivers installed?" << std::endl;
return 1;
}
//Open data files
device->getFileSystem()->addFileArchive("Resources",true,true,irr::io::EFAT_ZIP);
device->getFileSystem()->addFileArchive("Video.tar");
device->getFileSystem()->addFileArchive("Textures.tar");
device->getFileSystem()->addFileArchive("Models.tar");
device->getFileSystem()->addFileArchive("Audio.tar");
//Load first room.
loading* loadingScreen = new loading(device);
fps *fpsLevel = new fps();
pthread_t creationThread;
struct load_struct loadingStruct;
loadingStruct.device = device;
loadingStruct.fpsLevel = fpsLevel;
//pthread_create(creationThread,NULL,load,(void *)&loadingStruct); Fix me!
while(loading==false)
{
loadingScreen->update();
std::this_thread::sleep_for(std::chrono::milliseconds(1000/60));
}
loadingScreen->kill();
// Run first room.
fpsLevel->run();
//Clean up.
device->drop();
return 0;
}
Here's my loading.h file:
#include <irrlicht.h>
#include <string>
#ifndef LOADING_H
#define LOADING_H
class loading
{
public:
loading(irr::IrrlichtDevice *device);
void update();
void kill();
protected:
int slide;
static const int SLIDES_AMOUNT = 60;
const std::string FILENAME_TEMPLATE = "Loading_Screen/%.png";
irr::video::ITexture* slides[SLIDES_AMOUNT];
};
#endif // LOADING_H
Any help would be appreciated!
The problem might be caused by the fact that you've got two different things called loading:
First, there's your class loading. But then, there's also the variable bool loading. So when you do:
loading* loadingScreen = new loading(device);
the compiler is confronted with an ambiguity and seems to prefer the variable loading over the type loading (I'm not an C++ pro and can't explain why this is the case; my guess is it's because the variable is the most "recent" definition of that name).
The fix is pretty easy: make the two names different. Since C++ is case-sensitive you should adopt the convention of starting class names with an uppercase letter: make that class Loading and Loading* loadingScreen = new Loading(device);.
I searched and found various similar questions though I wasn't able to find a solution for my problem.
It's a SDL2 + OpenGL program, I can compile it with no problems in Linux using g++ 4.9.1 though not on Windows (VS 2013).
I get errors like:
Error 1 error LNK2005: "union SDL_Event e" (?e##3TSDL_Event##A) already defined in engine.obj PATH_TO_PROJECT\main.obj Game
for all the variables defined in the file engine.h:
//engine.h
#ifndef ENGINE_H
#define ENGINE_H
#include <SDL.h>
#include <SDL_opengl.h>
#include <iostream>
#include "player.cpp"
SDL_Event e;
bool running = true;
bool up = false, down = false, left = false, right = false;
bool attack = false;
player hero(20, 300, 50, 50, 10.0); //x, y, lenght, height, speed
void init(char* title, int WIDTH, int HEIGHT);
void draw(SDL_Window* screen, SDL_GLContext context, int WIDTH, int HEIGHT);
#endif
engine.cpp consists of:
//engine.cpp
#include "engine.h"
void init(int WIDTH, int HEIGHT) {
//BODY OF THE FUNCTION
}
void draw(SDL_Window* screen, SDL_GLContext context, int WIDTH, int HEIGHT) {
//BODY OF THE FUNCTION
}
main.cpp is the only file that includes engine.cpp:
//main.cpp
#include <SDL.h>
#include <SDL_opengl.h>
#include "engine.cpp"
#include <iostream>
#define WIDTH 800
#define HEIGHT 600
int main() {
SDL_Init(SDL_INIT_EVERYTHING);
STD::cout << "SDL started." << STD::endl;
init(WIDTH, HEIGHT);
//Create the window
SDL_Window *screen = SDL_CreateWindow("game title", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WIDTH, HEIGHT, SDL_WINDOW_OPENGL);
SDL_GLContext context = SDL_GL_CreateContext(screen);
draw(screen, context, WIDTH, HEIGHT);
SDL_Quit();
return 0;
}
I also get these:
Warning 9 warning LNK4098: defaultlib 'msvcrt.lib' conflicts with use of other libs; use /NODEFAULTLIB:library PATH_TO_PROJECT\MSVCRTD.lib(cinitexe.obj) Game
Error 10 error LNK2019: unresolved external symbol _SDL_main referenced in function _main PATH_TO_PROJECT\SDL2main.lib(SDL_windows_main.obj) Game
Error 11 error LNK1120: 1 unresolved externals PATH_TO_PROJECT\Game.exe Game
I can't really understand what is going on, specially since I can compile it on Linux, could someone please help me out?
The libraries are corrected linked, verified that. Also, if I put all the code in the main function and use only one file it compiles and runs with no problem.
Your main.cpp file should include engine.h, not engine.cpp.
Further, you do define the global e multiple times, in every translation unit that includes the engine.h header. In the engine.h header declare it extern instead, which tells the compiler that e exists somewhere but doesn't actually define it:
extern SDL_Event e;
Then define it in only one translation unit; put this in engine.cpp:
SDL_Event e;
You need to do the same thing with all of the other global variables as well. Note that you need to initialize them where you define them, so this goes in the header:
extern bool running, attack, up, down, left, right;
And this goes in engine.cpp:
bool running = true;
bool attack = false; /* and so on */
You are getting these redefinition issues as you have included .cpp file in your main:-
#include "engine.cpp"
You should always include header files only. ( Also these header files should contain appropriate header gaurds ).
I have a dll and I even have the header files for the dll, but I don't have the implementation neither the lib file for the dll. I try to load up the dll with the QLibrary class and get class instance from it. I successfully retrieved the class after 2hours, but when I try to call a function on the object I get unresolved external symbol which tells me that the dll did not exported properly. For simplicity I re-created the issue with the following sources:
DLL-Project (testlibrary_global.hpp):
#ifndef TESTLIBRARY_GLOBAL_HPP
#define TESTLIBRARY_GLOBAL_HPP
#include <QtCore/qglobal.h>
#if defined(TESTLIBRARY_LIBRARY)
# define TESTLIBRARYSHARED_EXPORT Q_DECL_EXPORT
#else
# define TESTLIBRARYSHARED_EXPORT Q_DECL_IMPORT
#endif
#endif // TESTLIBRARY_GLOBAL_HPP
DLL-Project (testlibrary.hpp):
#ifndef TESTLIBRARY_HPP
#define TESTLIBRARY_HPP
#include "testlibrary_global.hpp"
#include <QDebug>
class TESTLIBRARYSHARED_EXPORT TestLibrary {
public:
TestLibrary();
~TestLibrary();
void Test();
};
extern "C" TESTLIBRARYSHARED_EXPORT TestLibrary* getInstance();
#endif // TESTLIBRARY_HPP
DLL-Project (testlibrary.cpp):
#include "testlibrary.hpp"
TestLibrary::TestLibrary() {
qDebug() << "Constructor called!";
}
TestLibrary::~TestLibrary() {
qDebug() << "Destructor called!";
}
void Test() {
qDebug() << "Hello from library!";
}
TestLibrary *getInstance() {
return new TestLibrary();
}
This is very straight forward, does not contain anything fancy really. As you can see I kept the class default as the QtCreator does did not change anything, except added another function with extern "C" and the export defined in global. the purpose of this would be to get an object from the dll itself, (since I have the .h and .dll nothing else). Now for the loader application, again dirty yet simple basic stuff:
#include <QCoreApplication>
#include <QLibrary>
#include <QDebug>
#include "testlibrary.hpp"
int main(int argc, char *argv[]) {
QCoreApplication a(argc, argv);
QString libPath = QString("C:/Users/johorvat/Documents/QTProjects/build-TestLibrary-Desktop_Qt_5_2_0_MSVC2010_32bit_OpenGL-Debug/debug/TestLibrary.dll");
QLibrary lib(libPath);
bool loaded = lib.load();
QString error = lib.errorString();
qDebug() << "Loaded: " << loaded;
typedef TestLibrary* (*Prototype)();
Prototype Func = (Prototype) lib.resolve("getInstance");
if (Func) {
TestLibrary* tl = Func();
if (tl) {
qDebug() << "Yey, I gotta clazz!";
}
}
return a.exec();
}
I added the header file to the project because i have it anyway. I used QLibrary to load up the dll and retrieved the getInstance method from it with which I could get an instance of the TestLibrary class. However if I try to call the Test() method of TestLibrary within the if(tl) { ... } i get an unresolved external symbol error message that tells me it can't find the definition of the Test method.
What am I missing in here?
P.S.: I won't get lib files so let's focus on the problem with the dll loading :).
Regards,
Joey
Well since you've written void Test() { in your .cpp file and not void TestLibrary::Test { your function isn't being defined and so it isn't exported at all.
EDIT:
After this code like that works fine and prints "Hello" in qDebug (dll should be compiled in debug, I failed on that the first time)
QFunctionPointer raw = lib.resolve("?Test#TestLibrary##QEAAXXZ");
TestPrototype testFunc;
*(QFunctionPointer*) &testFunc = raw;
(tl->*testFunc) ();
Decorated function name is not very nice but I don't know what exactly can be done about it :) And also you'll get differently mangled names with different compilers so using Qt in this case will not be cross-platform anyway.