CreateProcess()

今天下午在寫程式的時候被一個奇怪的問題給卡住,我寫的程式去呼叫 CreateProcess() 但一直跳出下面的錯誤訊息:

ScreenHunter_02 Dec. 24 19.07

原來問題出在我使用 Windows 的 CreateProcess() 這個 api 的方法錯誤,先來偷看一下 MSDN 的函式原型

BOOL WINAPI CreateProcess(
  __in_opt     LPCTSTR lpApplicationName,
  __inout_opt  LPTSTR lpCommandLine,
  __in_opt     LPSECURITY_ATTRIBUTES lpProcessAttributes,
  __in_opt     LPSECURITY_ATTRIBUTES lpThreadAttributes,
  __in         BOOL bInheritHandles,
  __in         DWORD dwCreationFlags,
  __in_opt     LPVOID lpEnvironment,
  __in_opt     LPCTSTR lpCurrentDirectory,
  __in         LPSTARTUPINFO lpStartupInfo,
  __out        LPPROCESS_INFORMATION lpProcessInformation
);

其實問題的癥結很簡單,就出在第二個參數身上,為什麼 lpCommandLine 的型別是 LPTSTR 而不是 LPCTSTR 呢?理由很簡單,因為系統會去更改這個參數,所以 MSDN 也用了 __inout_opt 來修飾這個參數,因此我們不能傳一個 read only 的記憶體區塊到這個參數來。引用一下 MSDN 的說明:

The Unicode version of this function, CreateProcessW, can modify the contents of this string. Therefore, this parameter cannot be a pointer to read-only memory (such as a const variable or a literal string). If this parameter is a constant string, the function may cause an access violation.

有趣吧?!只有 Unicode 版本的 CreateProcess 會修改這個參數,所以下面的程式碼可以 work:

wchar_t cmd[ 100 ] = L"notepad D:\\mt.txt"; 
CreateProcessW( NULL, cmd, NULL, NULL, false, 0, NULL, NULL, &si, &pi ); 
CreateProcessA( NULL, "notepad D:\\mt.txt", NULL, NULL, false, 0, NULL, NULL, &si, &pi );

但下面的程式碼是不能 work

CreateProcessW( NULL, L"notepad D:\\mt.txt", NULL, NULL, false, 0, NULL, NULL, &si, &pi );

是不是有點不 consistent 呢?

此外,眼尖的人可能會發現為什麼一個 LPTSTR (即TCHAR*) 型別可以接受一個型別為 const TCHAR array 呢?C++ standard 2.13.4 不是這樣說的嗎?

A string literal is a sequence of characters (as defined in 2.13.2) surrounded by double quotes, optionally beginning with the letter L, as in "..."or L"...".  A string literal that does not begin with L is an ordinary string literal, also referred to as a narrow string literal.  An ordinary string literal has type “array of n const char” and static storage duration (3.7), where n is the size of the string as defined below, and is initialized with the given characters. A string literal that begins with L, such as L"asdf", is a wide string literal.  A wide string literal has type “array of n const wchar_t” and has static storage duration, where n is the size of the string as defined below, and is initialized with the given characters.

怎麼 VC++ 連個 warning 都不給呢?這是因為 C++ 為了相容於 C 所做出的讓步,來看一下 4.2  Array-to-pointer conversion 的描述:

A string literal (2.13.4) that is not a wide string literal can be converted to an rvalue of type “pointer to char”; a wide string literal can be converted to an rvalue of type “pointer to wchar_t”.  In either case, the result is a pointer to the first element of the array.  This conversion is considered only when there is an explicit appropriate pointer target type, and not when there is a general need to convert from an lvalue to an rvalue.

因此,比較好的習慣是:總是用 const char/wchar_t* 去指向一塊 literal string。Scott Meyer 不就說了嗎?

Use const whenever possible

: )

#include <iostream>
#include <typeinfo>
using namespace std;

void foo( char* msg )
{
    cout << "[foo( char* msg )] " << msg << endl;
}
void foo( const char* msg )
{
    cout << "[foo( const char* msg )] " << msg << endl;
}

template<typename T>
void printType( T* x )
{
    cout << "type of T: " << typeid( T ).name() << endl;
}

void badCall()
{
    throw "Exception";
}

int main()
{
    foo( "Hello World" );
    printType( "Hello World" );

    try {
        badCall();
    }
    catch ( const char* msg ) {
        cerr << "[const char* msg] " << msg << endl;
    }
    catch ( char* msg ) {
        cerr << "[char* msg] " << msg << endl;
    }

    return 0;
}

我可沒說上面的 code 可以順利 compile 唷~

4 則留言:

87showmin 提到...

以前只要用Windows API,看到一堆:LPXSTR的都當作是同一種東西,基本上就算有warning也不會理它。不過你說在CreateProcess時沒有出現warning,那整支程式在執行時是出現什麼問題?找不到D:\\mt.txt?
另外,文中有說填入const variable可能發生access violation,為何你的結論是Use const whenever possible?
我可能又漏看了什麼重點了,請周大師開示。 > <"

Keiko 提到...

我不是大師,我也正在找姓周的大師!!

1. 重看了一下文章,果然是我表達不好,其實問題在:CreateProcess() 的 unicode 版本會去修改第二個參數,因此它會要求這個參數不能是 read only 的記憶體。如果我們應塞了一個 read only 的記憶體給它,會產生一個 access violation 的 runtime error (晚點我會把文章更新,放上一個執行期錯誤的 screenshot)

2. 因為 CreateProcess() 會嘗試去修改第二個參數,因此遵循正規的介面設計法則,第二個參數的型別是 LPTSTR 而不是 LPCTSTR 。

此時,因為 C/C++ 都號稱自己是 strong type 的 language ,怎麼當我傳入一個 const pointer 到 LPTSTR 時卻不會有 const to non-const 的錯誤訊息呢?

a. 根據 C++ standard 4.2 的描述,當我們 explicit 把 literal string 從 const char/wchar_t * 轉到 char/wchar_t * 是合法的,因此:
CreateProcess( NULL, L"notepad D:\\mt.txt", NULL, NULL, false, 0, NULL, NULL, &si, &pi ); 是段合法的程式碼!

b. 而若是今天程式變成:
const wchar_t* cmd = L"notepad D:\\mt.txt";
reateProcess( NULL, cmd, NULL, NULL, false, 0, NULL, NULL, &si, &pi );

這樣就會是不合法,因為這是個 implicit 的 conversion,standard 並不允許。

所以整篇文章,其實只是我從 CreateProcess() 介面設計不當(?!)的痛苦中突然想到 C++ 對於 literal string 為了相容所作的讓為了相容所作的讓步,一時興起去看 standard 的心得,對不起,我跳 tone 了 XD

而最後的例子說 compiling 不過,是因為 standard 針對 throw expression 的 literal string 的 conversion 又有另一套作法(猜測因為這邊就不必為了跟 C 相容?!),理論上這程式碼可能編譯不過是因為:
1. exception filtering 時, const char* 會吃掉 char*
2. 然後 throw expression 是不允許 explicit conversion (上文 a. 的例子),但是 VC++ 2005 是允許,g++ 是不允許的~
(希望這個週末能有空再整理一下這邊,不好意思,原本要分享點心得,沒想到更困惱你了!)

Keiko 提到...

對了,忘了說,上面那個例子,也是為了是示範當我傳遞 literal string 當作 argument 時,function overloading 的解析,可以幫我們瞭解 literal string 的確是呼叫到 const char* 版本!

不過有趣的是,為什麼 typeid( T ).name() 卻是印出 char* 呢?這也是牽扯到 C++ standard 的規定,它會把 cv 修飾詞(const, volatile)都拿掉,因此透過 typeid() 去 get type 是不好的行為,其實這方面還有更多的討論,像是typeid().name()傳回的型別字串是長怎樣?大寫?小寫?有的沒的,都是 standard 沒有定義的,是 compiler-dependent ,或許,沒錯又是或許,哪天有機會可以整理一下這方面的 sharing …

ps. bbb 我 volitale 沒拼錯吧 XD

87showmin 提到...

講解的十分清楚,而且看懂意思後,我反而瞭解你原文想舖的梗是什麼。我懂的東西太少所以你一次要解釋一堆來龍去脈,真是辛苦了。:D

Windows + Visual Studio + VSCode + CMake 的疑難雜症

Environment Windows 10 Visual Studio 2019 CMake 3.27.7 VSCode VSCode CMake Tools 1. CMAKE_BUILD_TYPE 是空的 參考一下 這篇 的處理。 大致上因為 Visual...