Here's my try on the matter.
Well, pure C++ is better, but it's not a requirement. Portability would require explanation, and include at least any-reasonable-UNIX'ish OS and Windows. Preferably also Haiku, QNX, VXWorks... – einpoklum
I haven't used any external libraries, and everything under there should be C++11 compliant.
I've opted to use some cstring
functions, as well as fopen
/fclose
for file interactions, but feel free to substitute them with whatever suits you. I've mainly chosen them because it returns a FILE*
on all systems (didn't want to make a huge mess with Windows HANDLE
s)
The Windows part relies on Windows API calls. There's no way to avoid this, you need it to determine the conventional temporary folder.
The non-Windows part is for all POSIX-compliant systems, with Haiku, QNX and VXWorks should be, along with most if not all Linux-based systems and MacOS. I couldn't test on all those systems, but if you can find any incompatibility, please notify me.
Using preprocessor directives, we detect the OS family we're targetting. This allows us to define different code for Windows and POSIX-complicant systems.
Both sides define:
void getTempDir(char* pathBuff)
to get the path to the temp directory, with a trailer directory separator character.
void openTempFileFromFolder(const char* folderPath, char* outFilePath, FILE** outFileStream)
to create a new temporary file in said folder, and provides you with a FILE*
to write to it, and a char*
containing its filename.
void closeTempFile(FILE* file)
to close and delete the temporary file
void hookFunctionToClose()
to link the closureRoutine()
function to program termination events (like signals, exits, ...) so closeTempFile(FILE*)
can be called even if the program is terminated dirtily, as per your requirement:
delete the file on exit (success, failure and exception - cleanup is important!).
Do note that in the case of VERY dirty terminations (SIGTERM
on Linux/Unix, a kill with the task manager on Windows, ...), you obviously can't have any cleanup executed. Since we're writing to the OS' temporary folder though, it can be configured to automatically delete files (here for Unix/Linux, here for Windows)
A global std::map<FILE*, std::string>
named tempFileMap
is defined to keep track of the opened temporary files, so they can be disposed of in case the program terminates.
It is therefore important for you to call closeTempFile(FILE*)
if you want to get rid of a temporary file yourself, and NOT fclose(FILE*)
. closeTempFile(FILE*)
will call fclose(FILE*)
internally, but will also delete the file and remove it from the list. Calling fclose(FILE*)
yourself will result in the file being closed twice!
GetTempPath2A
is used to get the temp folder
Since there's no existing function to generate a random file name, we use CoCreateGuid
to generate a GUID and we use it as a filename
We use GetFileAttributesA
to make sure the generated filename isn't used. If it is, we generate a new one.
The closure routine is bound to the program's exit using atexit
, which on Windows is also called in the case of a CTRL+C exit or when clicking the top right X on the window. It is also bound to the SIGABRT
signal using signal
.
The temp directory is gotten from environment variables, or defaults to /tmp
.
We use mkstemp
to create the temporary filename, which does the whole "keep generating a new filename until you find an unused one" process by itself. As an added bonus, it creates the file with permissions set up in a way so only we can touch the file.
The closure routine is bound to the program's exit using atexit
, which on POSIX systems is not called in the case of SIGINT
signams. We therefore manually both the SIGABRT
and SIGINT
signals using signal
to an exit
call, which does call our routine thanks to atexit
.
#include <string>
// std::filesystem::temp_directory_path is from C++17 (https://en.cppreference.com/w/cpp/filesystem/temp_directory_path), we need to call APIs ourselves to comply with C++11
void closureRoutine();
#include <map>
#include <vector>
std::map<FILE*, std::string> tempFileMap = {};
#if defined(_WIN32) || defined(WIN32)
#include <Windows.h>
#include <signal.h>
#if !defined(MAX_PATH)
#error MAX_PATH is not defined
#endif
#define PATH_MAX (MAX_PATH + 1)
void getTempDir(char* pathBuff) { // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppath2a
GetTempPath2A(PATH_MAX, pathBuff); // Has a trailing backslash by default
}
void openTempFileFromFolder(const char* folderPath, char* outFilePath, FILE** outFileStream) {
GUID guid;
while (true) {
if (CoCreateGuid(&guid) != S_OK)
break;
sprintf_s(outFilePath, PATH_MAX, "%s%08X%04X%04X%02X%02X%02X%02X%02X%02X%02X%02X", folderPath, guid.Data1, guid.Data2, guid.Data3, guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]);
// If the file doesn't exist yet, create it and return. Microsoft says a GUID cannot be generated twice, but for the same of mimicking the behaviour of POSIX's mkstemp, and for absolute rigorousness, let's check anyway.
DWORD tmpFileAttrib = GetFileAttributesA(outFilePath); // https://stackoverflow.com/a/6218957/9399492
if (tmpFileAttrib == INVALID_FILE_ATTRIBUTES) {
fopen_s(outFileStream, outFilePath, "w+");
tempFileMap.insert({ *outFileStream, outFilePath });
return;
}
}
*outFileStream = nullptr;
}
void closeTempFile(FILE* file) {
std::string filename = tempFileMap[file];
tempFileMap.erase(file);
fclose(file);
DeleteFileA(filename.c_str());
}
void __closureRoutine(int sig) {
closureRoutine();
}
void hookFunctionToClose() {
signal(SIGABRT, __closureRoutine);
atexit(closureRoutine); // Naturally triggered by CTRL+C on Windows
}
#elif defined (__unix__) || (defined (__APPLE__) && defined (__MACH__)) // Should work for any POSIX-compliant system, including MacOS (https://stackoverflow.com/a/11351171/9399492)
#include <cstring>
#include <cstdlib>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#if !defined(PATH_MAX)
#error PATH_MAX is not defined
#endif
void getTempDir(char* pathBuff) { // https://stackoverflow.com/a/14626770/9399492
char* tmp = getenv("TMPDIR");
if (!tmp) {
tmp = getenv("TMP");
if (!tmp) {
tmp = getenv("TEMP");
if (!tmp) {
tmp = getenv("TEMPDIR");
}
}
}
sprintf(pathBuff, "%s/", tmp ? tmp : "/tmp"); // Add the trailing slash
}
void openTempFileFromFolder(const char* folderPath, char* outFilePath, FILE** outFileStream) {
sprintf(outFilePath, "%sXXXXXX", folderPath);
int fd = mkstemp(outFilePath); // Creates a file with umask 600. It is guaranteed to be unused, since it should keep trying to create a file with a new name until it can (https://en.wikipedia.org/wiki/Mkstemp#Mechanism)
*outFileStream = (fd < 0) ? nullptr : fdopen(fd, "w+"); // Gets a FILE stream for our newly created file
if (fd >= 0) {
*outFileStream = fdopen(fd, "w+"); // Gets a FILE stream for our newly created file
tempFileMap.insert({ *outFileStream, outFilePath });
}
else
*outFileStream = nullptr;
}
void closeTempFile(FILE* file) {
std::string filename = tempFileMap[file];
tempFileMap.erase(file);
unlink(filename.c_str()); // "unlink" will get the file to be deleted as soon as it's closed: https://stackoverflow.com/a/47033463/9399492
fclose(file);
}
void __closureRoutine(int sig) {
exit((sig == SIGINT) ? 130 : 134); // triggers the atexit callback
}
void hookFunctionToClose() {
signal(SIGABRT, __closureRoutine);
signal(SIGINT, __closureRoutine);
atexit(closureRoutine);
}
#else
#error Unsupported system
#endif
// This function is gonna get triggered when the program exists. We use it to clean up our temporary files.
// Do note that it won't be called in the case of forced exits, like by sending SIGTERM on POSIX-compliant systems, or forcefully ending the task in the Task Manager on Windows.
// In such cases, the temporary folder will have to be cleared manually.
// See https://serverfault.com/q/377348 for Unix/Linux and https://superuser.com/a/296827 for Windows
void closureRoutine() {
// Storing all FILE pointers into a vector beforehand to avoid a "cannot dereference value-initialized map/set iterator" error
std::vector<FILE*> pairs;
for (auto it = tempFileMap.begin(); it != tempFileMap.end(); ++it) {
pairs.push_back(it->first);
}
for (FILE*& file : pairs) {
std::string filename = tempFileMap[file];
closeTempFile(file);
printf("Automatically closed & cleaned up %s\n", filename.c_str());
}
}
int main() {
// Hook our closure routine to events related to program termination; this will let us clean after ourselves.
hookFunctionToClose();
// Get the current temporary directory.
// On POSIX-based systems, it will often be /tmp.
// On Windows, it's usually gonna be C:\Users\%USERNAME%\AppData\Local\Temp, or C:\Temp, depending of your priviledges.
char tempFolderBuff[PATH_MAX] {};
getTempDir(tempFolderBuff);
printf("Found temporary folder: %s\n", tempFolderBuff);
// tempFolderBuff now contains the temporary directory path WITH a trailing directory separator char. This means we only need to append our temporary filename to it.
char tempFileBuff[PATH_MAX]{};
FILE* file;
openTempFileFromFolder(tempFolderBuff, tempFileBuff, &file); // Keeps tempFolderBuff intact and writes the temp filename to tempFileBuff instead, so we can open multiple temporary files easily
printf("Opened temporary file: %s\n", tempFileBuff);
if (!file) {
// Handle the case where we couldn't create a new temporary file
/* ... */
exit(1);
}
/* Do whatever you want with your file */
closeTempFile(file);
printf("Manually closed & cleaned up %s\n", tempFileBuff);
openTempFileFromFolder(tempFolderBuff, tempFileBuff, &file); // Open another one to test automatic cleanup
printf("Opened temporary file: %s\n", tempFileBuff);
if (!file) {
// Handle the case where we couldn't create a new temporary file
/* ... */
exit(1);
}
/* Do whatever you want with your file */
abort(); // Even though we aren't manually closing & cleaning up the file, it should be done automatically.
return 0;
}