如何将Visual Studio宏值传递到预处理指令中?

11

在我的项目中,我需要在运行时访问 $(SolutionDir) 宏的值。为此,我尝试添加预处理器条目,如 DEBUG_ROOT=$(SolutionDir)DEBUG_ROOT=\"$(SolutionDir)\",但由于 $(SolutionDir) 包含单一的 \ 字符(例如$(SolutionDir) = c:\users\lukas\desktop\sandbox\),导致各种编译器错误,因为转义序列无效。

有没有一种简单的方法将 $(SolutionDir) 宏的值传递到我的代码中?

背景

我在我的调试构建中使用了函数 OutputDebugString(..) 很多次来查看我的代码正在执行什么。

/* debug.h */
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
#define LOCATION __FILE__ "(" TOSTRING(__LINE__) ") : "

#if !defined(DEBUG_ROOT)
#define DEBUG_ROOT    "#"   /* escape string to force strstr(..) to fail */
#endif

/*
**  DBGMSG macro setting up and writing a debug string.
**  Note: copying the strings together is faster than calling OutputDebugString(..) several times!
**  Todo: Ensure that size of dbgStr is not exceeded!!!
*/
#define DBGMSG(text) \
    { \
        char dbgStr[1024]; \
        char *pFile; \
        pFile = strstr(LOCATION, DEBUG_ROOT); \
        if (pFile == LOCATION) \
        { \
            wsprintf(dbgStr, ".%s", pFile + strlen(DEBUG_ROOT)); \
        } \
        else \
        { \
            wsprintf(dbgStr, "%s", LOCATION); \
        } \
        wsprintf(dbgStr, "%s%s", dbgStr, text); \
        OutputDebugString(dbgStr); \
    }


/* somewhere in the code */
DBGMSG("test")

使用该代码片段将在Visual Studio的输出窗口中打印出类似于c:\users\lukas\desktop\sandbox\testconsole\main.c(17) : test的内容。这加速了查找导致打印的代码位置,因为您只需双击输出窗口中的行,Visual Studio就会自动跳转到指定的代码位置。

由于取决于解决方案的位置(__FILE__扩展为绝对路径),调试字符串的“头”可能会变得非常长。我发现Visual Studio足够聪明,能够理解相对路径,比如相对于解决方案根目录。为了缩短字符串的长度,我正在检查__FILE__是否在DEBUG_ROOT目录中,如果是,则将DEBUG_ROOT替换为简单的'.',以生成到DEBUG_ROOT的相对路径。因此,如果我写#define DEBUG_ROOT "c:\\users\\lukas\\desktop\\sandbox",则上面示例的最终调试字符串将为.\testconsole\main.c(17) : test。目前,我正在预处理器定义中设置DEBUG_ROOT的值。

由于有几个人正在项目上工作,因此在项目设置中使用绝对路径并不明智,因为每个团队成员可能将源文件检出到不同的根目录。因此,我尝试使用$(SolutionDir)宏创建类似于DEBUG_ROOT=\"$(SolutionDir)\\"的内容。但是这样做会遇到问题。由于$(SolutionDir) = c:\users\lukas\desktop\sandbox\,扩展DEBUG_ROOT会导致未定义的转义序列、未终止的字符串和许多丑陋的编译器错误...

解决方案

根据kfsone的答案,我想出了以下解决方案,使得可以将任何Visual Studio宏(如$(SolutionDir))的值传递到您的代码中。以下解决方案与使用的Visual Studio版本和语言C/C++无关。

在项目的预处理器条目中添加SOLUTION_DIR=\"$(SolutionDir)"将导致编译器命令行如下:

/Od /D "WIN32" /D "_DEBUG" /D "_CONSOLE" /D "SOLUTION_DIR=\"C:\Users\Lukas\Desktop\sandbox\""
/Gm /EHsc /RTC1 /MDd /Fo"Debug\\" /Fd"Debug\vc80.pdb" /W3 /nologo /c /Wp64 /ZI /TP
/errorReport:prompt
请注意,$(SolutionDir) 前面有一个 \" 来创建 " 字符,但是以单个 " 结束。查看编译器的命令行显示,终止的 " 是由 $(SolutionDir) 的最后一个 \ 转义的。
在代码中使用 SOLUTION_DIR 会导致未知的转义序列,并且该字符串中所有的 \ 字符都被删除。这是由编译器执行的,它展开了 SOLUTION_DIR 并将 \ 解释为转义序列的开始。
使用我上面发布的代码中的 TOSTRING(x) 宏可以解决这个问题,因为它强制编译器使用原始字符串而不进行进一步处理。
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)

#define SOLUTION_DIR2   TOSTRING(SOLUTION_DIR)

// the following line may cause compiler warnings (unrecognized character escape sequence)
printf("%s\n", SOLUTION_DIR);    // prints C:UsersLukasDesktopsandbox 

// the following line compiles without any warnings
printf("%s\n", SOLUTION_DIR2);   // prints "C:\Users\Lukas\Desktop\sandbox"

从这里开始,只需进行一些字符串操作即可从SOLUTION_DIR2中删除"字符。


从你的问题中可以看出,你能够在C++中使用$SolutionDir,但是只有反斜杠存在问题。是这样吗?如果是这样,你是如何做到的(我无法弄清楚)。另外,你使用的VS版本是什么? - Motti
嗯,不是$(SolutionDir)。你的main.c存储在项目目录中。当然,对于库项目中的代码是行不通的。你需要在运行时修剪路径,通过argv[0]和GetCurrentDirectory()可以轻松获取项目目录。 - Hans Passant
1
看起来你只需要在启动时或者第一次调用日志函数时正确转义DEBUG_ROOT并使用该变量即可。 - Retired Ninja
@HansPassant:我已经发布了整个代码,它可以在运行时修剪路径。你是正确的,我犯了一个复制粘贴错误,我已经纠正了。使用argv [0]GetCurrentDirectory()会引入某种初始化,这也有缺点,即Visual Studio无法找到嵌套库的文件。这就是为什么我将解决方案目录用作DEBUG_ROOT的原因,这将确保所有调试消息被正确修剪。因此,我正在寻找一种将解决方案目录传递给我的代码的方法,但我担心使用$(SolutionDir)宏不起作用。 - Lukas Thomsen
@RetiredNinja:好主意!我尝试了以下代码:char *debugRoot = DEBUG_ROOT; 并使用调试器查看了其内容。似乎Visual Studio在编译时已经转义了 $(SolutionDir) 的值,因为所有的 '\' 字符都消失了,而且没有办法恢复它 :( - Lukas Thomsen
显示剩余4条评论
1个回答

13

Visual Studio 2013及以上版本提供了C++11特性——原始字符串字面值,可以实现此功能。语法如下:

'R"' <delimiter> '(' <string> ')' <delimiter> '"'

例如,如果您选择“?:?”作为分隔符。
R"?:?(don't\escape)?:?"

或者如果您选择“Foo123”

R"Foo123(don't\escape)Foo123"

但是在这个演示中,我将选择使用 ? 作为单字符分隔符,因为我们知道它在 Windows 文件名中是非法的。

现在,您可以设置项目级别的预处理器定义:

DIR=R"?(C:\\Temp\\)?"

并且以下代码将生成预期的输出。
#include <iostream>
int main() {
    std::cout << DIR << '\n';
}

写入

C:\\Temp\\

替代

C:\Temp\

现在,要捕获SolutionDir宏很简单,只需要:
DIR=R"?($(SolutionDir))?"

如果这很麻烦,您可以在属性表中添加自定义宏。转到“属性浏览器”,右键单击您的项目,添加一个新的属性表,将其命名为“ProjectMacros.props”或其他名称。
展开您的项目并选择其中一个配置,例如debug,双击“PropertySheet”值以打开“PropertySheet PropertyPages”,然后选择“UserMacros”
点击“添加宏”。
Name: RawSolutionDir
Value: R"?path?($(SolutionDir))?path?"

您现在应该能够使用预处理器入口了。
SOLUTIONDIR=$(RawSolutionDir)

关于一项我之前不知道的功能,这是非常有趣的信息!我尝试了您的解决方案,在Visual Studio 2013和C++下完美运行。不幸的是,我的大量C代码导致SOLUTIONDIR = "?"。但是,您的建议给了我一些想法...如果它们有效,我会在这里分享它们。 - Lukas Thomsen
我接受了你的答案,因为它是一个可行的解决方案,并帮助我创建了一个不依赖于Visual Studio版本和C++语言的解决方案。谢谢你的帮助! - Lukas Thomsen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接