Udostępnij za pośrednictwem


Przewodnik przenoszenia: Narzędzie Spy++

Ta analiza przypadku przenoszenia została zaprojektowana tak, aby przedstawić, jak wygląda typowy projekt przenoszenia, typy problemów, które mogą wystąpić, oraz niektóre ogólne wskazówki i wskazówki dotyczące rozwiązywania problemów z przenoszeniem. Nie jest to ostateczny przewodnik przenoszenia, ponieważ środowisko przenoszenia projektu zależy bardzo od specyfiki kodu.

Spy++

Spy++ to powszechnie używane narzędzie diagnostyczne graficznego interfejsu użytkownika dla pulpitu systemu Windows, które udostępnia wszelkiego rodzaju informacje o elementach interfejsu użytkownika na pulpicie systemu Windows. Przedstawia pełną hierarchię okien i zapewnia dostęp do metadanych dotyczących każdego okna i kontrolki. Ta przydatna aplikacja jest dostarczana z programem Visual Studio od wielu lat. Znaleźliśmy starą wersję, która została ostatnio skompilowana w programie Visual C++ 6.0 i przeportowaliśmy ją do programu Visual Studio 2015. Środowisko programu Visual Studio 2017 lub Visual Studio 2019 powinno być niemal identyczne.

Uważaliśmy, że ten przypadek jest typowy dla przenoszenia aplikacji klasycznych systemu Windows korzystających z MFC i interfejsu API Win32, zwłaszcza w przypadku starych projektów, które nie zostały zaktualizowane w każdej wersji programu Visual C++ od wersji Visual C++ 6.0.

Krok 1. Konwertowanie pliku projektu.

Plik projektu, dwa stare pliki .dsw z programu Visual C++ 6.0, można łatwo przekonwertować bez problemów, które wymagają dalszej uwagi. Jednym z projektów jest aplikacja Spy++. Drugi to SpyHk, napisany w języku C, pomocniczej biblioteki DLL. Bardziej złożone projekty mogą nie być tak łatwe, jak opisano tutaj.

Po uaktualnieniu dwóch projektów nasze rozwiązanie wyglądało następująco:

Zrzut ekranu przedstawiający program Spy plus rozwiązanie.

Mamy dwa projekty, jeden z dużą liczbą plików C++, a drugi bibliotekę DLL napisaną w języku C.

Krok 2. Problemy z plikiem nagłówka

Podczas kompilowania nowo przekonwertowanego projektu jedną z pierwszych rzeczy, które często znajdziesz, jest to, że nie można odnaleźć plików nagłówkowych używanych przez projekt.

Jednym z plików, których nie można znaleźć w programie Spy++ było verstamp.h. Z wyszukiwania w Internecie ustaliliśmy, że pochodzi to z zestawu DAO SDK, przestarzałej technologii danych. Chcieliśmy dowiedzieć się, jakie symbole były używane z tego pliku nagłówka, aby sprawdzić, czy ten plik był naprawdę potrzebny, czy te symbole zostały zdefiniowane gdzie indziej, więc skomentowaliśmy deklarację pliku nagłówka i ponownie skompilowano. Okazuje się, że jest tylko jeden symbol, który jest potrzebny, VER_FILEFLAGSMASK.

1>C:\Program Files (x86)\Windows Kits\8.1\Include\shared\common.ver(212): error RC2104: undefined keyword or key name: VER_FILEFLAGSMASK

Najprostszym sposobem znalezienia symbolu w dostępnych plikach dołączania jest użycie funkcji Znajdź w plikach (Ctrl+Shift+F) i określenie katalogów dołączania języka Visual C++. Znaleźliśmy go w ntverp.h. Zamieniliśmy plik verstamp.h na ntverp.h i ten błąd zniknął.

Krok 3. Ustawienie Pliku wyjściowego konsolidatora

Starsze projekty czasami mają pliki umieszczone w niekonwencjonalnych lokalizacjach, które mogą powodować problemy po uaktualnieniu. W takim przypadku musimy dodać $(SolutionDir) do właściwości projektu Ścieżkę dołączenia , aby upewnić się, że program Visual Studio może znaleźć w nim pliki nagłówkowe, a nie w jednym z folderów projektu.

Program MSBuild skarży się, że właściwość Link.OutputFile nie jest zgodna z wartościami TargetPath i TargetName , wydając MSB8012.

warning MSB8012: TargetPath(...\spyxx\spyxxhk\.\..\Debug\SpyxxHk.dll) does not match the Linker's OutputFile property value (...\spyxx\Debug\SpyHk55.dll). This may cause your project to build incorrectly. To correct this, please make sure that $(OutDir), $(TargetName) and $(TargetExt) property values match the value specified in %(Link.OutputFile).warning MSB8012: TargetName(SpyxxHk) does not match the Linker's OutputFile property value (SpyHk55). This may cause your project to build incorrectly. To correct this, please make sure that $(OutDir), $(TargetName) and $(TargetExt) property values match the value specified in %(Link.OutputFile).

Link.OutputFile to dane wyjściowe kompilacji (na przykład EXE, DLL) i zwykle są tworzone z $(TargetDir)$(TargetName)$(TargetExt)pliku , podając ścieżkę, nazwę pliku i rozszerzenie. Jest to typowy błąd podczas migrowania projektów ze starego narzędzia kompilacji Visual C++ (vcbuild.exe) do nowego narzędzia kompilacji (MSBuild.exe). Ponieważ w programie Visual Studio 2010 nastąpiła zmiana narzędzia kompilacji, ten problem może wystąpić za każdym razem, gdy migrujesz projekt przed 2010 do wersji 2010 lub nowszej. Podstawowym problemem jest to, że kreator migracji projektu nie aktualizuje wartości Link.OutputFile , ponieważ nie zawsze jest możliwe określenie, jaka powinna być jego wartość na podstawie innych ustawień projektu. W związku z tym zazwyczaj trzeba je ustawić ręcznie. Aby uzyskać więcej informacji, zobacz ten wpis na blogu języka Visual C++.

W tym przypadku właściwość Link.OutputFile w przekonwertowanym projekcie została ustawiona na wartość .\Debug\Spyxx.exe i .\Release\Spyxx.exe dla projektu Spy++, w zależności od konfiguracji. Najlepszym rozwiązaniem jest po prostu zastąpienie tych zakodowanych na stałe wartości za pomocą $(TargetDir)$(TargetName)$(TargetExt) opcji Wszystkie konfiguracje. Jeśli to nie zadziała, możesz dostosować je z tego miejsca lub zmienić właściwości w sekcji Ogólne , w której te wartości są ustawione (właściwości to Katalog wyjściowy, Nazwa docelowa i Rozszerzenie obiektu docelowego. Należy pamiętać, że jeśli wyświetlana właściwość korzysta z makr, możesz wybrać pozycję Edytuj na liście rozwijanej, aby wyświetlić okno dialogowe z ostatnim ciągiem z wykonanymi podstawieniami makr. Wszystkie dostępne makra i ich bieżące wartości można wyświetlić, wybierając przycisk Makra .

Krok 4. Aktualizowanie docelowej wersji systemu Windows

Następny błąd wskazuje, że wersja WINVER nie jest już obsługiwana w MFC. WinVER dla systemu Windows XP jest 0x0501.

C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxv_w32.h(40): fatal error C1189: #error:  MFC does not support WINVER less than 0x0501.  Please change the definition of WINVER in your project properties or precompiled header.

System Windows XP nie jest już obsługiwany przez firmę Microsoft, więc mimo że jest to dozwolone w programie Visual Studio, należy wycofać obsługę w aplikacjach i zachęcić użytkowników do przyjęcia nowych wersji systemu Windows.

Aby pozbyć się błędu, zdefiniuj winVER, aktualizując ustawienie Właściwości projektu na najniższą wersję systemu Windows, która ma być obecnie docelowa. Znajdź tabelę wartości dla różnych wersji systemu Windows tutaj.

Plik stdafx.h zawierał niektóre z tych definicji makr.

#define WINVER       0x0500  // these defines are set so that we get the
#define _WIN32_WINNT 0x0500  // maximum set of message/flag definitions,
#define _WIN32_IE    0x0400  // from both winuser.h and commctrl.h.

WINVER ustawimy system Windows 7. Łatwiej jest odczytać kod później, jeśli używasz makra dla systemu Windows 7 (_WIN32_WINNT_WIN7), a nie samej wartości (0x0601).

#define WINVER _WINNT_WIN32_WIN7 // Minimum targeted Windows version is Windows 7

Krok 5. Błędy konsolidatora

Dzięki tym zmianom kompiluje się projekt SpyHk (DLL), ale generuje błąd konsolidatora.

LINK : warning LNK4216: Exported entry point _DLLEntryPoint@12

Punkt wejścia dla biblioteki DLL nie powinien być eksportowany. Punkt wejścia ma być wywoływany tylko przez moduł ładujący, gdy biblioteka DLL jest najpierw załadowana do pamięci, więc nie powinna znajdować się w tabeli eksportu, która jest przeznaczona dla innych wywołujących. Musimy tylko upewnić się, że nie ma dołączonej __declspec(dllexport) do niej dyrektywy. W pliku spyxxhk.c musimy usunąć go z dwóch miejsc, deklarację i definicję DLLEntryPoint. Nigdy nie miała sensu używać tej dyrektywy, ale poprzednie wersje konsolidatora i kompilatora nie oznaczyły jej jako problemu. Nowsze wersje konsolidatora dają ostrzeżenie.

// deleted __declspec(dllexport)
BOOL WINAPI DLLEntryPoint(HINSTANCE hinstDLL,DWORD fdwReason, LPVOID lpvReserved);

Projekt biblioteki DLL języka C, SpyHK.dll, teraz kompiluje i łączy bez błędów.

Krok 6. Więcej nieaktualnych plików nagłówków

W tym momencie zaczniemy pracę nad głównym projektem wykonywalny Spyxx.

Nie można odnaleźć kilku innych plików dołączanych: ctl3d.h i penwin.h. Chociaż wyszukiwanie w Internecie może być przydatne, aby spróbować zidentyfikować, co zawiera nagłówek, czasami informacje nie są tak przydatne. Dowiedzieliśmy się, że ctl3d.h był częścią zestawu Exchange Development Kit i zapewnił obsługę określonego stylu kontrolek w systemie Windows 95, a penwin.h odnosi się do window Pen Computing, przestarzałego interfejsu API. W tym przypadku po prostu oznaczymy wiersz jako komentarz i zajmiemy się #include niezdefiniowanym symbolami, tak jak w przypadku verstamp.h. Wszystko, co dotyczy kontrolek 3D lub Pen Computing, zostało usunięte z projektu.

Biorąc pod uwagę projekt z wieloma błędami kompilacji, które stopniowo eliminujesz, nie jest realistyczne znalezienie wszystkich zastosowań nieaktualnego interfejsu API od razu po usunięciu #include dyrektywy. Nie wykryliśmy go natychmiast, ale raczej w pewnym późniejszym momencie doszliśmy do błędu, który WM_DLGBORDER był niezdefiniowany. Jest to tylko jeden wiele niezdefiniowanych symboli, które pochodzą z ctl3d.h. Po ustaleniu, że odnosi się on do przestarzałego interfejsu API, usunęliśmy wszystkie odwołania w kodzie do niego.

Krok 7. Aktualizowanie starego kodu iostreams

Następny błąd jest typowy w przypadku starego kodu C++, który używa strumieni iostream.

mstream.h(40): fatal error C1083: Cannot open include file: 'iostream.h': No such file or directory

Problem polega na tym, że stara biblioteka iostreams została usunięta i zastąpiona. Musimy zastąpić stare strumienie iostream nowszymi standardami.

#include <iostream.h>
#include <strstrea.h>
#include <iomanip.h>

Są to zaktualizowane następujące elementy:

#include <iostream>
#include <sstream>
#include <iomanip>

W przypadku tej zmiany występują problemy z ostrstreamprogramem , który nie jest już używany. Odpowiedni zamiennik jest ostringstream. Staramy się dodać element typedef , ostrstream aby uniknąć zbyt dużej modyfikacji kodu, co najmniej na początku.

typedef std::basic_ostringstream<TCHAR> ostrstream;

Obecnie projekt jest kompilowany przy użyciu MBCS (zestaw znaków wielobajtowych), więc char jest odpowiednim typem danych znaków. Jednak aby umożliwić łatwiejszą aktualizację kodu do formatu UTF-16 Unicode, zaktualizujemy go TCHARdo char wchar_t , który jest rozpoznawany jako lub w zależności od tego, czy właściwość Zestaw znaków w ustawieniach projektu jest ustawiona na MBCS lub Unicode.

Należy zaktualizować kilka innych fragmentów kodu. Zamieniliśmy klasę ios bazową na ios_base, a zastąpiliśmy element ostream przez basic_ostream<T>. Dodajemy dwie dodatkowe definicje typów, a ta sekcja jest kompilowana.

typedef std::basic_ostream<TCHAR> ostream;
typedef ios_base ios;

Użycie tych definicji typów jest tylko tymczasowym rozwiązaniem. Aby uzyskać bardziej trwałe rozwiązanie, możemy zaktualizować każde odwołanie do zmienionego lub nieaktualnego interfejsu API.

Oto następny błąd.

error C2039: 'freeze': is not a member of 'std::basic_stringbuf<char,std::char_traits<char>,std::allocator<char>>'

Następnym problemem jest to, że basic_stringbuf nie ma freeze metody. Metoda freeze służy do zapobiegania wyciekowi pamięci w starym ostreampliku . Nie potrzebujemy go teraz, gdy używamy nowego ostringstreamelementu . Możemy usunąć wywołanie metody freeze.

//rdbuf()->freeze(0);

Następne dwa błędy wystąpiły w sąsiednich wierszach. Pierwszy narzeka na użycie metody ends, która jest manipulatorem we/wy starej iostream biblioteki, który dodaje terminator o wartości null do ciągu. Drugi z tych błędów wyjaśnia, że dane wyjściowe str metody nie mogą być przypisane do wskaźnika innego niż const.

// Null terminate the string in the buffer and
// get a pointer to it.
//
*this << ends;
LPSTR psz = str();
2>mstream.cpp(167): error C2065: 'ends': undeclared identifier2>mstream.cpp(168): error C2440: 'initializing': cannot convert from 'std::basic_string<char,std::char_traits<char>,std::allocator<char>>' to 'LPSTR'

Użycie nowej biblioteki strumienia nie jest potrzebne, ends ponieważ ciąg jest zawsze zakończony wartością null, aby można było usunąć wiersz. W przypadku drugiego problemu problem polega na tym, że teraz str() nie zwraca wskaźnika do tablicy znaków dla ciągu; zwraca std::string typ. Rozwiązaniem drugiej jest zmiana typu na LPCSTR i użycie c_str() metody w celu zażądania wskaźnika.

//*this << ends;
LPCTSTR psz = str().c_str();

Błąd, który nas zadziwił przez jakiś czas w tym kodzie.

MOUT << _T(" chUser:'") << chUser
<< _T("' (") << (INT)(UCHAR)chUser << _T(')');

Makro MOUT rozpoznaje, do *g_pmout którego jest obiekt typu mstream. Klasa mstream pochodzi z standardowej klasy ciągów wyjściowych, std::basic_ostream<TCHAR>. Jednak w przypadku _T wokół literału ciągu, który umieszczamy w przygotowaniu do konwersji na Unicode, rozpoznawanie przeciążenia operatora << kończy się niepowodzeniem z następującym komunikatem o błędzie:

1>winmsgs.cpp(4612): error C2666: 'mstream::operator <<': 2 overloads have similar conversions
1>  c:\source\spyxx\spyxx\mstream.h(120): note: could be 'mstream &mstream::operator <<(ios &(__cdecl *)(ios &))'
1>  c:\source\spyxx\spyxx\mstream.h(118): note: or       'mstream &mstream::operator <<(ostream &(__cdecl *)(ostream &))'
1>  c:\source\spyxx\spyxx\mstream.h(116): note: or       'mstream &mstream::operator <<(ostrstream &(__cdecl *)(ostrstream &))'
1>  c:\source\spyxx\spyxx\mstream.h(114): note: or       'mstream &mstream::operator <<(mstream &(__cdecl *)(mstream &))'
1>  c:\source\spyxx\spyxx\mstream.h(109): note: or       'mstream &mstream::operator <<(LPTSTR)'
1>  c:\source\spyxx\spyxx\mstream.h(104): note: or       'mstream &mstream::operator <<(TCHAR)'
1>  c:\source\spyxx\spyxx\mstream.h(102): note: or       'mstream &mstream::operator <<(DWORD)'
1>  c:\source\spyxx\spyxx\mstream.h(101): note: or       'mstream &mstream::operator <<(WORD)'
1>  c:\source\spyxx\spyxx\mstream.h(100): note: or       'mstream &mstream::operator <<(BYTE)'
1>  c:\source\spyxx\spyxx\mstream.h(95): note: or       'mstream &mstream::operator <<(long)'
1>  c:\source\spyxx\spyxx\mstream.h(90): note: or       'mstream &mstream::operator <<(unsigned int)'
1>  c:\source\spyxx\spyxx\mstream.h(85): note: or       'mstream &mstream::operator <<(int)'
1>  c:\source\spyxx\spyxx\mstream.h(83): note: or       'mstream &mstream::operator <<(HWND)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(1132): note: or       'CDumpContext &operator <<(CDumpContext &,COleSafeArray &)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(1044): note: or       'CArchive &operator <<(CArchive &,ATL::COleDateTimeSpan)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(1042): note: or       'CDumpContext &operator <<(CDumpContext &,ATL::COleDateTimeSpan)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(1037): note: or       'CArchive &operator <<(CArchive &,ATL::COleDateTime)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(1035): note: or       'CDumpContext &operator <<(CDumpContext &,ATL::COleDateTime)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(1030): note: or       'CArchive &operator <<(CArchive &,COleCurrency)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(1028): note: or       'CDumpContext &operator <<(CDumpContext &,COleCurrency)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(955): note: or       'CArchive &operator <<(CArchive &,ATL::CComBSTR)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(951): note: or       'CArchive &operator <<(CArchive &,COleVariant)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(949): note: or       'CDumpContext &operator <<(CDumpContext &,COleVariant)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxwin.h(248): note: or       'CArchive &operator <<(CArchive &,const RECT &)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxwin.h(247): note: or       'CArchive &operator <<(CArchive &,POINT)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxwin.h(246): note: or       'CArchive &operator <<(CArchive &,SIZE)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxwin.h(242): note: or       'CDumpContext &operator <<(CDumpContext &,const RECT &)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxwin.h(241): note: or       'CDumpContext &operator <<(CDumpContext &,POINT)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxwin.h(240): note: or       'CDumpContext &operator <<(CDumpContext &,SIZE)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afx.h(1639): note: or       'CArchive &operator <<(CArchive &,const CObject *)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afx.h(1425): note: or       'CArchive &operator <<(CArchive &,ATL::CTime)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afx.h(1423): note: or       'CDumpContext &operator <<(CDumpContext &,ATL::CTime)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afx.h(1418): note: or       'CArchive &operator <<(CArchive &,ATL::CTimeSpan)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afx.h(1416): note: or       'CDumpContext &operator <<(CDumpContext &,ATL::CTimeSpan)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\ostream(694): note: or       'std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &std::operator <<<wchar_t,std::char_traits<wchar_t>>(std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &,const char *)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\ostream(741): note: or       'std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &std::operator <<<wchar_t,std::char_traits<wchar_t>>(std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &,char)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\ostream(866): note: or       'std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &std::operator <<<wchar_t,std::char_traits<wchar_t>>(std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &,const _Elem *)'
1>          with
1>          [
1>              _Elem=wchar_t
1>          ]
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\ostream(983): note: or       'std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &std::operator <<<wchar_t,std::char_traits<wchar_t>,wchar_t[10]>(std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &&,const _Ty (&))'
1>          with
1>          [
1>              _Ty=wchar_t [10]
1>          ]
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\ostream(1021): note: or       'std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &std::operator <<<wchar_t,std::char_traits<wchar_t>>(std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &,const std::error_code &)'
1>  winmsgs.cpp(4612): note: while trying to match the argument list '(CMsgStream, const wchar_t [10])'

Istnieje tak wiele definicji operatorów << , że ten rodzaj błędu może być zastraszający. Po dokładniejszym przyjrzeniu się dostępnym przeciążeniom możemy zobaczyć, że większość z nich jest nieistotna i przyjrzymy mstream się bliżej definicji klasy, zidentyfikowaliśmy następującą funkcję, którą uważamy za wywołaną w tym przypadku.

mstream& operator<<(LPTSTR psz)
{
  return (mstream&)ostrstream::operator<<(psz);
}

Powodem, dla którego nie jest wywoływana, jest to, że literał ciągu ma typ const wchar_t[10] , jak widać w ostatnim wierszu tego długiego komunikatu o błędzie, więc konwersja na wskaźnik inny niż const nie jest automatyczna. Jednak ten operator nie powinien modyfikować parametru wejściowego, więc bardziej odpowiedni typ parametru to LPCTSTR (const char* podczas kompilowania jako MBCS i const wchar_t* Jako Unicode), a nie LPTSTR (char* podczas kompilowania jako MBCS i wchar_t* jako Unicode). Wprowadzenie tej zmiany spowoduje usunięcie tego błędu.

Ten typ konwersji był dozwolony w starszych, mniej rygorystycznych kompilatorach, ale nowsze zmiany zgodności wymagają bardziej poprawnego kodu.

Krok 8. Bardziej rygorystyczne konwersje kompilatora

Występuje również wiele błędów, takich jak następujące:

error C2440: 'static_cast': cannot convert from 'UINT (__thiscall CHotLinkCtrl::* )(CPoint)' to 'LRESULT (__thiscall CWnd::* )(CPoint)'

Błąd występuje na mapie komunikatów, która jest po prostu makrem:

BEGIN_MESSAGE_MAP(CFindToolIcon, CWnd)
// other messages omitted...
ON_WM_NCHITTEST() // Error occurs on this line.
END_MESSAGE_MAP()

Po przejściu do definicji tego makra zobaczymy, że odwołuje się do funkcji OnNcHitTest.

#define ON_WM_NCHITTEST() \
{ WM_NCHITTEST, 0, 0, 0, AfxSig_l_p, \
(AFX_PMSG)(AFX_PMSGW) \
(static_cast< LRESULT (AFX_MSG_CALL CWnd::*)(CPoint) > (&ThisClass :: OnNcHitTest)) },

Problem ma związek z niezgodnością w wskaźniku do typów funkcji składowych. Problem nie jest konwersją typu CHotLinkCtrl klasy na CWnd typ klasy jako typ klasy, ponieważ jest to prawidłowa konwersja pochodna na bazę. Problem jest zwracany typ: UINT a LRESULT. Funkcja LRESULT jest rozpoznawana jako LONG_PTR, która jest wskaźnikiem 64-bitowym lub wskaźnikiem 32-bitowym, w zależności od docelowego typu binarnego, więc funkcja UINT nie konwertuje na ten typ. Nie jest to rzadkością podczas uaktualniania kodu napisanego przed 2005 r., ponieważ zwracany typ wielu metod mapy komunikatów zmienił się z UINT na LRESULT w programie Visual Studio 2005 w ramach zmian zgodności 64-bitowych. Zmieniamy typ zwracany z UINT w następującym kodzie na LRESULT:

afx_msg UINT OnNcHitTest(CPoint point);

Po zmianie mamy następujący kod:

afx_msg LRESULT OnNcHitTest(CPoint point);

Ponieważ istnieje około dziesięciu wystąpień tej funkcji w różnych klasach pochodnych od CWnd, warto użyć funkcji Przejdź do definicji (Klawiatura: F12) i Przejdź do deklaracji (Klawiatura: Ctrl+F12), gdy kursor znajduje się na funkcji w edytorze, aby je zlokalizować i przejść do nich z okna narzędzia Znajdź symbol. Przejdź do pozycji Definicja jest zwykle bardziej przydatna dla tych dwóch elementów. Przejdź do deklaracji , aby znaleźć deklaracje inne niż deklaracja klasy definiującej, takie jak deklaracje klasy przyjaznej lub odwołania do przodu.

Krok 9. Zmiany MFC

Następny błąd odnosi się również do zmienionego typu deklaracji, a także występuje w makrze.

error C2440: 'static_cast': cannot convert from 'void (__thiscall CFindWindowDlg::* )(BOOL,HTASK)' to 'void (__thiscall CWnd::* )(BOOL,DWORD)'

Problem polega na tym, że drugi parametr CWnd::OnActivateApp zmiany z HTASK na DWORD. Ta zmiana miała miejsce w wersji 2002 programu Visual Studio, programu Visual Studio .NET.

afx_msg void OnActivateApp(BOOL bActive, HTASK hTask);

Musimy odpowiednio zaktualizować deklaracje OnActivateApp w klasach pochodnych w następujący sposób:

afx_msg void OnActivateApp(BOOL bActive, DWORD dwThreadId);

W tym momencie możemy skompilować projekt. Istnieje jednak kilka ostrzeżeń, które należy wykonać, i istnieją opcjonalne części uaktualnienia, takie jak konwertowanie z MBCS na Unicode lub ulepszanie zabezpieczeń przy użyciu funkcji Secure CRT.

Krok 10. Rozwiązywanie problemów z ostrzeżeniami kompilatora

Aby uzyskać pełną listę ostrzeżeń, należy wykonać ponowne kompilowanie wszystkich w rozwiązaniu, a nie zwykłą kompilację, aby upewnić się, że wszystko, co wcześniej skompilowane, zostanie ponownie skompilowane, ponieważ uzyskasz tylko raporty ostrzegawcze z bieżącej kompilacji. Drugie pytanie brzmi, czy zaakceptować bieżący poziom ostrzeżenia, czy użyć wyższego poziomu ostrzeżenia. W przypadku przenoszenia dużej ilości kodu, zwłaszcza starego kodu, użycie wyższego poziomu ostrzeżenia może być odpowiednie. Możesz również zacząć od domyślnego poziomu ostrzeżenia, a następnie zwiększyć poziom ostrzeżenia, aby uzyskać wszystkie ostrzeżenia. Jeśli używasz metody /Wall, w plikach nagłówków systemu są wyświetlane ostrzeżenia, dlatego wiele osób używa /W4 metody , aby uzyskać najwięcej ostrzeżeń dotyczących ich kodu bez otrzymywania ostrzeżeń dotyczących nagłówków systemu. Jeśli chcesz, aby ostrzeżenia wyświetlane jako błędy, dodaj /WX opcję . Te ustawienia znajdują się w sekcji C/C++ okna dialogowego Właściwości projektu.

Jedna z metod w CSpyApp klasie generuje ostrzeżenie dotyczące funkcji, która nie jest już obsługiwana.

void SetDialogBkColor() {CWinApp::SetDialogBkColor(::GetSysColor(COLOR_BTNFACE));}

Ostrzeżenie jest następujące.

warning C4996: 'CWinApp::SetDialogBkColor': CWinApp::SetDialogBkColor is no longer supported. Instead, handle WM_CTLCOLORDLG in your dialog

Komunikat WM_CTLCOLORDLG był już obsługiwany w kodzie Spy++, więc jedyną wymaganą zmianą było usunięcie odwołań do SetDialogBkColorelementu , które nie jest już potrzebne.

Następne ostrzeżenie było proste, aby naprawić, komentując nazwę zmiennej. Otrzymaliśmy następujące ostrzeżenie:

warning C4456: declaration of 'lpszBuffer' hides previous local declaration

Kod, który tworzy ten kod, obejmuje makro.

DECODEPARM(CB_GETLBTEXT)
{
  P2WPOUT();

  P2LPOUTPTRSTR;
  P2IFDATA()
  {
    PARM(lpszBuffer, PPACK_STRINGORD, ED2);

    INDENT();

    P2IFISORD(lpszBuffer)
    {
      P2OUTORD(lpszBuffer);
    }
    else
    {
      PARM(lpszBuffer, LPTSTR, ED2);
      P2OUTS(lpszBuffer);
    }
  }
}

Duże wykorzystanie makr, ponieważ w tym kodzie zwykle utrudnia konserwację kodu. W tym przypadku makra zawierają deklaracje zmiennych. Makro PARM jest definiowane w następujący sposób:

#define PARM(var, type, src)type var = (type)src

W związku z tym zmienna lpszBuffer jest zadeklarowana dwukrotnie w tej samej funkcji. Nie jest tak proste, aby rozwiązać ten problem, tak jakby kod nie używał makr (po prostu usunąć drugą deklarację typu). Tak jak to jest, mamy niefortunny wybór konieczności podjęcia decyzji, czy przepisać kod makra jako zwykły kod (żmudne i prawdopodobnie podatne na błędy) lub wyłączyć ostrzeżenie.

W tym przypadku zdecydujemy się wyłączyć ostrzeżenie. Możemy to zrobić, dodając pragma w następujący sposób:

#pragma warning(disable : 4456)

Wyłączenie ostrzeżenia może spowodować ograniczenie efektu wyłączania tylko do kodu, który generuje ostrzeżenie, aby uniknąć pomijania ostrzeżenia, gdy może on dostarczyć przydatnych informacji. Dodamy kod, aby przywrócić ostrzeżenie tuż po wierszu, który go generuje, lub jeszcze lepiej, ponieważ to ostrzeżenie występuje w makrze, użyj słowa kluczowego __pragma , które działa w makrach (#pragma nie działa w makrach).

#define PARM(var, type, src)__pragma(warning(disable : 4456))  \
type var = (type)src \
__pragma(warning(default : 4456))

Następne ostrzeżenie wymaga pewnych poprawek kodu. Interfejs API GetVersion Win32 (i GetVersionEx) jest przestarzały.

warning C4996: 'GetVersion': was declared deprecated

Poniższy kod przedstawia sposób uzyskiwania wersji.

// check Windows version and set m_bIsWindows9x/m_bIsWindows4x/m_bIsWindows5x flags accordingly.
DWORD dwWindowsVersion = GetVersion();

Następuje po nim wiele kodu, który analizuje wartość dwWindowsVersion, aby określić, czy używamy systemu Windows 95 i której wersji systemu Windows NT. Ponieważ to wszystko jest nieaktualne, usuwamy kod i zajmujemy się wszelkimi odwołaniami do tych zmiennych.

Artykuł Wersja systemu operacyjnego zmienia się w systemach Windows 8.1 i Windows Server 2012 R2 wyjaśnia sytuację.

Istnieją metody w CSpyApp klasie, które odpytują wersję systemu operacyjnego: IsWindows9x, IsWindows4xi IsWindows5x. Dobrym punktem wyjścia jest założenie, że wersje systemu Windows, które zamierzamy obsługiwać (Windows 7 i nowsze) są bliskie Windows NT 5, jeśli chodzi o technologie używane przez tę starszą aplikację. Zastosowanie tych metod dotyczyło ograniczeń starszych systemów operacyjnych. Zmieniliśmy więc te metody tak, aby zwracały wartość TRUE dla IsWindows5x i FALSE dla innych.

BOOL IsWindows9x() {/*return(m_bIsWindows9x);*/ return FALSE;  }
BOOL IsWindows4x() {/*return(m_bIsWindows4x);*/ return FALSE;  }
BOOL IsWindows5x() {/*return(m_bIsWindows5x);*/ return TRUE;  }

Pozostawiło to tylko kilka miejsc, w których zmienne wewnętrzne były używane bezpośrednio. Ponieważ usunęliśmy te zmienne, otrzymujemy kilka błędów, które muszą radzić sobie jawnie.

error C2065: 'm_bIsWindows9x': undeclared identifier
void CSpyApp::OnUpdateSpyProcesses(CCmdUI *pCmdUI)
{
  pCmdUI->Enable(m_bIsWindows9x || hToolhelp32 != NULL);
}

Możemy zastąpić to wywołaniem metody lub po prostu przekazać wartość TRUE i usunąć stary specjalny przypadek dla systemu Windows 9x.

void CSpyApp::OnUpdateSpyProcesses(CCmdUI *pCmdUI)
{
  pCmdUI->Enable(TRUE /*!m_bIsWindows9x || hToolhelp32 != NULL*/);
}

Ostateczne ostrzeżenie na poziomie domyślnym (3) musi mieć związek z polem bitowym.

treectl.cpp(1656): warning C4463: overflow; assigning 1 to bit-field that can only hold values from -1 to 0

Kod, który wyzwala ten kod, jest następujący.

m_bStdMouse = TRUE;

Deklaracja m_bStdMouse wskazuje, że jest to pole bitowe.

class CTreeListBox : public CListBox
{
  DECLARE_DYNCREATE(CTreeListBox)

  CTreeListBox();

  private:
  int ItemFromPoint(const CPoint& point);

  class CTreeCtl* m_pTree;
  BOOL m_bGotMouseDown : 1;
  BOOL m_bDeferedDeselection : 1;
  BOOL m_bStdMouse : 1;

Ten kod został napisany przed typem wbudowanym wartości logicznej był obsługiwany w języku Visual C++. W takim kodzie wartość BOOL była wartością typedef dla intelementu . Typ int jest typem, a bitową reprezentacją signed elementu signed int jest użycie pierwszego bitu jako bitu znaku, więc pole bitowe typu int może być interpretowane jako reprezentujące 0 lub -1, prawdopodobnie nie to, co było zamierzone.

Nie wiesz, patrząc na kod, dlaczego są to pola bitowe. Czy intencją było zachowanie rozmiaru obiektu małego, czy też w dowolnym miejscu, w którym jest używany układ binarny obiektu? Zmieniliśmy je na zwykłych członków BOOL, ponieważ nie widzieliśmy żadnego powodu użycia pola bitowego. Użycie pól bitowych w celu zachowania małego rozmiaru obiektu nie jest gwarantowane. Zależy to od sposobu, w jaki kompilator określa typ.

Możesz się zastanawiać, czy użycie standardowego typu bool w całym przypadku byłoby pomocne. Wiele starych wzorców kodu, takich jak typ BOOL, zostały wynalezione w celu rozwiązania problemów, które zostały później rozwiązane w standardowym języku C++, więc zmiana z BOOL na bool wbudowany typ jest tylko jednym przykładem takiej zmiany, którą należy rozważyć po dokonaniu kodu początkowo uruchomionego w nowej wersji.

Gdy zajmiemy się wszystkimi ostrzeżeniami wyświetlanymi na poziomie domyślnym (poziom 3), zmieniliśmy się na poziom 4, aby przechwycić kilka dodatkowych ostrzeżeń. Pierwszy z nich pojawił się w następujący sposób:

warning C4100: 'nTab': unreferenced formal parameter

Kod, który wygenerował to ostrzeżenie, był następujący.

virtual void OnSelectTab(int nTab) {};

Wydaje się to wystarczająco nieszkodliwe, ale ponieważ chcieliśmy czystej kompilacji z /W4 i /WX ustawić, po prostu skomentowaliśmy nazwę zmiennej, pozostawiając ją ze względu na czytelność.

virtual void OnSelectTab(int /*nTab*/) {};

Inne otrzymane ostrzeżenia były przydatne do ogólnego czyszczenia kodu. Istnieje wiele niejawnych konwersji z int lub unsigned int do programu WORD (który jest definicją typu dla unsigned shortelementu ). Obejmują one możliwą utratę danych. W tych przypadkach dodaliśmy rzutowanie do programu WORD.

Kolejne ostrzeżenie na poziomie 4, które otrzymaliśmy dla tego kodu, to:

warning C4211: nonstandard extension used: redefined extern to static

Problem występuje, gdy zmienna została najpierw zadeklarowana extern, a następnie zadeklarowana staticpóźniej . Znaczenie tych dwóch specyfikatorów klas magazynu jest wzajemnie wykluczające się, ale jest to dozwolone jako rozszerzenie firmy Microsoft. Jeśli chcesz, aby kod był przenośny do innych kompilatorów lub chcesz go skompilować za pomocą /Za (zgodność ANSI), należy zmienić deklaracje tak, aby miały zgodne specyfikatory klas magazynu.

Krok 11. Przenoszenie z MBCS do Unicode

Pamiętaj, że w świecie systemu Windows, gdy mówimy Unicode, zwykle oznaczamy UTF-16. Inne systemy operacyjne, takie jak Linux, używają utF-8, ale system Windows ogólnie nie. Wersja MFC MBCS została uznana za przestarzałą w programach Visual Studio 2013 i 2015, ale nie jest już przestarzała w programie Visual Studio 2017. Jeśli korzystasz z programu Visual Studio 2013 lub 2015, przed wykonaniem kroku przenoszenia kodu MBCS do formatu UTF-16 Unicode możemy tymczasowo wyeliminować ostrzeżenia, że MBCS jest przestarzałe, aby wykonać inną pracę lub odłożyć przenoszenie do wygodnego czasu. Bieżący kod używa MBCS i aby kontynuować, musimy zainstalować wersję MFC ANSI/MBCS. Dość duża biblioteka MFC nie jest częścią domyślnej instalacji programu Visual Studio Desktop z instalacją języka C++ , dlatego należy ją wybrać z opcjonalnych składników instalatora. Zobacz dodatek MFC MBCS DLL. Po pobraniu i ponownym uruchomieniu programu Visual Studio możesz skompilować i połączyć się z wersją MFC MBCS, ale aby pozbyć się ostrzeżeń dotyczących MBCS, jeśli używasz programu Visual Studio 2013 lub 2015, należy również dodać NO_WARN_MBCS_MFC_DEPRECATION do listy wstępnie zdefiniowanych makr w sekcji Preprocesor właściwości projektu, lub na początku pliku nagłówka stdafx.h lub innego wspólnego pliku nagłówka.

Mamy teraz pewne błędy konsolidatora.

fatal error LNK1181: cannot open input file 'mfc42d.lib'

LNK1181 występuje, ponieważ nieaktualna wersja biblioteki statycznej mfc jest dołączana do danych wejściowych konsolidatora. Nie jest to już wymagane, ponieważ możemy dynamicznie łączyć MFC, dlatego wystarczy usunąć wszystkie biblioteki statyczne MFC z właściwości Input w sekcji Konsolidator właściwości projektu. Ten projekt używa /NODEFAULTLIB również opcji , a zamiast tego wyświetla listę wszystkich zależności biblioteki.

msvcrtd.lib;msvcirtd.lib;kernel32.lib;user32.lib;gdi32.lib;advapi32.lib;Debug\SpyHk55.lib;%(AdditionalDependencies)

Teraz zaktualizujmy stary kod zestawu znaków wielobajtowych (MBCS) na Unicode. Ponieważ jest to aplikacja systemu Windows, ściśle powiązana z platformą klasyczną systemu Windows, przeprowadzimy ją do formatu UTF-16 Unicode używanego przez system Windows. Jeśli piszesz międzyplatformowy kod lub przenosisz aplikację systemu Windows na inną platformę, warto rozważyć przenoszenie do formatu UTF-8, który jest powszechnie używany w innych systemach operacyjnych.

Przenoszenie do formatu UTF-16 Unicode, musimy zdecydować, czy nadal chcemy skompilować opcję MBCS, czy nie. Jeśli chcemy mieć opcję obsługi MBCS, należy użyć makra TCHAR jako typu znaku, który jest rozpoznawany jako char lub wchar_t, w zależności od tego, czy _MBCS lub _UNICODE jest zdefiniowany podczas kompilacji. Przełączenie do języka TCHAR i wersji TCHAR różnych interfejsów API zamiast wchar_t skojarzonych z nim interfejsów API oznacza, że możesz wrócić do wersji MBCS kodu po prostu definiując makro _MBCS zamiast _UNICODE. Oprócz TCHAR istnieje wiele wersji TCHAR, takich jak powszechnie używane definicje typów, makra i funkcje. Na przykład LPCTSTR zamiast LPCSTR itd. W oknie dialogowym właściwości projektu w obszarze Właściwości konfiguracji w sekcji Ogólne zmień właściwość Zestaw znaków z Użyj zestawu znaków MBCS na Użyj zestawu znaków Unicode. To ustawienie ma wpływ na to, które makro jest wstępnie zdefiniowane podczas kompilacji. Istnieje zarówno makro UNICODE, jak i makro _UNICODE. Właściwość projektu ma wpływ na obie metody spójnie. Nagłówki systemu Windows używają formatu UNICODE, w którym nagłówki Visual C++, takie jak MFC, używają _UNICODE, ale gdy jeden jest zdefiniowany, drugi jest zawsze definiowany.

Istnieje dobry przewodnik przenoszenia z MBCS do UTF-16 Unicode przy użyciu języka TCHAR. Wybieramy tę trasę. Najpierw zmienimy właściwość Zestaw znaków na Użyj zestawu znaków Unicode i ponownie skompilujemy projekt.

Niektóre miejsca w kodzie były już używane przez TCHAR, najwyraźniej w oczekiwaniu na ostatecznie obsługę Unicode. Niektórzy nie byli. Wyszukaliśmy wystąpienia znaku CHAR, które jest elementem typedef , i zamieniliśmy większość z nich na charTCHAR. Ponadto szukaliśmy elementu sizeof(CHAR). Za każdym razem, gdy zmieniliśmy znak na TCHAR, zwykle musieliśmy zmienić wartość na sizeof(TCHAR) , ponieważ często było to używane do określenia liczby znaków w ciągu. Użycie nieprawidłowego typu nie powoduje wystąpienia błędu kompilatora, dlatego warto zwrócić trochę uwagi na ten przypadek.

Ten typ błędu jest bardzo powszechny tuż po przełączeniu do formatu Unicode.

error C2664: 'int wsprintfW(LPWSTR,LPCWSTR,...)': cannot convert argument 1 from 'CHAR [16]' to 'LPWSTR'

Oto przykład kodu, który tworzy następujący kod:

wsprintf(szTmp, "%d.%2.2d.%4.4d", rmj, rmm, rup);

Umieszczamy _T wokół literału ciągu, aby usunąć błąd.

wsprintf(szTmp, _T("%d.%2.2d.%4.4d"), rmj, rmm, rup);

Makro _T ma wpływ na skompilowanie literału ciągu jako char ciągu lub wchar_t ciągu, w zależności od ustawienia MBCS lub UNICODE. Aby zastąpić wszystkie ciągi _T w programie Visual Studio, najpierw otwórz pole Szybkie zamienianie (Klawiatura: Ctrl+F) lub Zamień w plikach (Klawiatura: Ctrl+Shift+H), a następnie zaznacz pole wyboru Użyj wyrażeń regularnych. Wprowadź ((\".*?\")|('.+?')) tekst wyszukiwania i _T($1) jako tekst zastępczy. Jeśli masz już makro _T wokół niektórych ciągów, ta procedura zostanie ponownie dodana i może również znajdować przypadki, w których nie chcesz _T, takich jak w przypadku użycia #includemetody , dlatego najlepiej użyć polecenia Zastąp dalej , a nie Zamień wszystko.

Ta konkretna funkcja, wsprintf, jest faktycznie zdefiniowana w nagłówkach systemu Windows, a dokumentacja zaleca, aby nie była używana, ze względu na możliwe przepełnienie buforu. Dla buforu nie podano żadnego rozmiaru szTmp , więc nie ma możliwości sprawdzenia, czy bufor może przechowywać wszystkie dane do zapisania w nim. Zobacz następną sekcję dotyczącą przenoszenia do protokołu Secure CRT, w której rozwiązujemy inne podobne problemy. W końcu zastąpiliśmy go _stprintf_s.

Innym typowym błędem, który zostanie wyświetlony podczas konwertowania na Unicode, jest to.

error C2440: '=': cannot convert from 'char *' to 'TCHAR *'

Kod, który go generuje, jest następujący:

pParentNode->m_szText = new char[strTitle.GetLength() + 1];
_tcscpy(pParentNode->m_szText, strTitle);

Mimo że _tcscpy funkcja została użyta, która jest funkcją strcpy TCHAR do kopiowania ciągu, przydzielony bufor był buforem char . Jest to łatwo zmieniane na TCHAR.

pParentNode->m_szText = new TCHAR[strTitle.GetLength() + 1];
_tcscpy(pParentNode->m_szText, strTitle);

Podobnie zmieniliśmy LPSTR (wskaźnik długi na STRing) i LPCSTR (wskaźnik długi na stałą string) na LPTSTR (wskaźnik długi na TCHAR STRing) i LPCTSTR (wskaźnik długi do stałej TCHAR STRing) odpowiednio, gdy jest to uzasadnione przez błąd kompilatora. Wybraliśmy, aby nie wprowadzać takich zamian przy użyciu wyszukiwania globalnego i zastępowania, ponieważ każda sytuacja musiała zostać zbadana indywidualnie. W niektórych przypadkach wersja jest poszukiwana, na przykład char podczas przetwarzania niektórych komunikatów systemu Windows, które używają struktur systemu Windows, które mają sufiks A . W interfejsie API systemu Windows sufiks A oznacza ASCII lub ANSI (a także dotyczy MBCS), a sufiks W oznacza znaki szerokie lub UTF-16 Unicode. Ten wzorzec nazewnictwa jest używany w nagłówkach systemu Windows, ale stosujemy go również w kodzie Spy++, gdy musieliśmy dodać wersję Unicode funkcji, która została już zdefiniowana tylko w wersji MBCS.

W niektórych przypadkach musieliśmy zastąpić typ, aby użyć wersji, która poprawnie rozpozna (na przykład WNDCLASSA zamiast WNDCLASSA).

W wielu przypadkach musieliśmy użyć wersji ogólnej (makro) interfejsu API Win32, takiego jak GetClassName (zamiast GetClassNameA). W instrukcji przełącznika obsługi komunikatów niektóre komunikaty są specyficzne dla MBCS lub Unicode, w takich przypadkach musieliśmy zmienić kod, aby jawnie wywołać wersję MBCS, ponieważ zamieniliśmy ogólnie nazwane funkcje na funkcje specyficzne dla A i W i dodano makro dla nazwy ogólnej, która rozpoznaje poprawną nazwę A lub W na podstawie tego, czy kod UNICODE jest zdefiniowany. W wielu częściach kodu po przełączeniu się w celu zdefiniowania _UNICODE wersja W jest teraz wybierana nawet wtedy, gdy wersja A jest żądana.

Istnieje kilka miejsc, w których trzeba było podjąć specjalne działania. Każde użycie polecenia lub MultiByteToWideChar może wymagać bliższego WideCharToMultiByte przyjrzenia się. Oto jeden przykład, w którym WideCharToMultiByte był używany.

BOOL C3dDialogTemplate::GetFont(CString& strFace, WORD& nFontSize)
{
  ASSERT(m_hTemplate != NULL);

  DLGTEMPLATE* pTemplate = (DLGTEMPLATE*)GlobalLock(m_hTemplate);
  if ((pTemplate->style & DS_SETFONT) == 0)
  {
    GlobalUnlock(m_hTemplate);
    return FALSE;
  }

  BYTE* pb = GetFontSizeField(pTemplate);
  nFontSize = *(WORD*)pb;
  pb += sizeof (WORD);
  WideCharToMultiByte(CP_ACP, 0, (LPCWSTR)pb, -1,
  strFace.GetBufferSetLength(LF_FACESIZE), LF_FACESIZE, NULL, NULL);
  strFace.ReleaseBuffer();
  GlobalUnlock(m_hTemplate);
  return TRUE;
}

Aby rozwiązać ten problem, musieliśmy zrozumieć, że powodem, dla którego zostało to zrobione, było skopiowanie szerokiego ciągu znaków reprezentującego nazwę czcionki do wewnętrznego buforu CStringelementu , strFace. Wymaga to nieco innego kodu dla ciągów wielobajtowych CString , jak w przypadku ciągów znaków szerokich CString , dlatego dodaliśmy w tym przypadku element #ifdef .

#ifdef _MBCS
WideCharToMultiByte(CP_ACP, 0, (LPCWSTR)pb, -1,
strFace.GetBufferSetLength(LF_FACESIZE), LF_FACESIZE, NULL, NULL);
strFace.ReleaseBuffer();
#else
wcscpy(strFace.GetBufferSetLength(LF_FACESIZE), (LPCWSTR)pb);
strFace.ReleaseBuffer();
#endif

Oczywiście zamiast wcscpy tak naprawdę należy używać wcscpy_swersji , która jest bezpieczniejsza. Następna sekcja dotyczy tego.

Aby sprawdzić naszą pracę, należy zresetować zestaw znaków do używania zestawu znaków wielobajtowych i upewnić się, że kod nadal kompiluje się przy użyciu MBCS, a także Unicode. Nie trzeba powiedzieć, że należy wykonać pełny test w ponownie skompilowanej aplikacji po wszystkich tych zmianach.

W naszej pracy z tym rozwiązaniem Spy++ trwało około dwóch dni roboczych dla przeciętnego dewelopera języka C++, aby przekonwertować kod na Unicode. Nie obejmowało to czasu ponownego testowania.

Krok 12. Przenoszenie do używania protokołu Secure CRT

Przenoszenie kodu w celu używania bezpiecznych wersji (wersji z sufiksem _s ) funkcji CRT jest następne. W takim przypadku ogólną strategią jest zastąpienie funkcji wersją _s , a następnie, zwykle, dodanie wymaganych dodatkowych parametrów rozmiaru buforu. W wielu przypadkach jest to proste, ponieważ rozmiar jest znany. W innych przypadkach, gdy rozmiar nie jest natychmiast dostępny, konieczne jest dodanie dodatkowych parametrów do funkcji korzystającej z funkcji CRT lub być może sprawdzenie użycia buforu docelowego i sprawdzenie, jakie są odpowiednie limity rozmiaru.

Język Visual C++ udostępnia sztuczkę ułatwiając zabezpieczanie kodu bez dodawania jak największej liczby parametrów rozmiaru i przy użyciu przeciążeń szablonu. Ponieważ te przeciążenia są szablonami, są dostępne tylko podczas kompilowania jako C++, a nie jako C. Spyxxhk jest projektem C, więc sztuczka nie będzie dla tego działać. Jednak Spyxx nie jest i możemy użyć sztuczki. Sztuczka polega na dodaniu wiersza podobnego do tego w miejscu, w którym zostanie skompilowany w każdym pliku projektu, na przykład w pliku stdafx.h:

#define _CRT_SECURE_TEMPLATE_OVERLOADS 1

Podczas definiowania tego, gdy bufor jest tablicą, a nie nieprzetworzonym wskaźnikiem, jego rozmiar jest wnioskowany z typu tablicy i jest używany jako parametr rozmiaru bez konieczności podawania go. Pomaga to zmniejszyć złożoność ponownego zapisywania kodu. Nadal trzeba zastąpić nazwę funkcji wersją _s , ale często można to zrobić za pomocą operacji wyszukiwania i zastępowania.

Zwracane wartości niektórych funkcji uległy zmianie. Na przykład _itoa_s (i _itow_s makro _itot_s) zwraca kod błędu (errno_t), a nie ciąg. W takich przypadkach należy przenieść wywołanie do _itoa_s oddzielnego wiersza i zastąpić je identyfikatorem buforu.

Niektóre z typowych przypadków: w przypadku memcpypolecenia , podczas przełączania do memcpy_sprogramu często dodaliśmy rozmiar kopiowanych struktur. Podobnie w przypadku większości ciągów i rozmiar tablicy lub buforu jest łatwo określany z deklaracji buforu lub przez znalezienie miejsca, w którym bufor został pierwotnie przydzielony. W niektórych sytuacjach należy określić, jak duży bufor jest rzeczywiście dostępny, a jeśli te informacje nie są dostępne w zakresie funkcji, którą modyfikujesz, należy dodać jako dodatkowy parametr, a kod wywołujący powinien zostać zmodyfikowany w celu dostarczenia informacji.

Dzięki tym technikom konwersja kodu na korzystanie z bezpiecznych funkcji CRT zajęła około pół dnia. Jeśli zdecydujesz się nie przeciążać szablonu i ręcznie dodać parametry rozmiaru, prawdopodobnie zajmie to dwa lub trzy razy więcej czasu.

Krok 13. /Zc:forScope— jest przestarzały

Ponieważ program Visual C++ 6.0, kompilator jest zgodny z bieżącym standardem, który ogranicza zakres zmiennych zadeklarowanych w pętli do zakresu pętli. Opcja kompilatora /Zc:forScope (Force Conformance for Loop Scope we właściwościach projektu) określa, czy jest to zgłaszane jako błąd. Powinniśmy zaktualizować nasz kod tak, aby był zgodny, i dodać deklaracje tuż poza pętlą. Aby uniknąć wprowadzania zmian w kodzie, możesz zmienić to ustawienie w sekcji Language (Język ) właściwości projektu języka C++ na No (/Zc:forScope-). Należy jednak pamiętać, że /Zc:forScope- może zostać usunięta w przyszłej wersji programu Visual C++, więc ostatecznie kod będzie musiał zmienić się tak, aby był zgodny ze standardem.

Te problemy są stosunkowo łatwe do rozwiązania, ale w zależności od kodu może to mieć wpływ na wiele kodu. Oto typowy problem.

int CPerfTextDataBase::NumStrings(LPCTSTR mszStrings) const
{
  for (int n = 0; mszStrings[0] != 0; n++)
  mszStrings = _tcschr(mszStrings, 0) + 1;
  return(n);
}

Powyższy kod generuje błąd:

'n': undeclared identifier

Dzieje się tak, ponieważ kompilator wycofał opcję kompilatora, która zezwalała na kod, który nie jest już zgodny ze standardem C++. W standardzie deklarowanie zmiennej wewnątrz pętli ogranicza jej zakres tylko do pętli, dlatego powszechna praktyka użycia licznika pętli poza pętlą wymaga również przeniesienia deklaracji licznika poza pętlę, jak w następującym poprawionym kodzie:

int CPerfTextDataBase::NumStrings(LPCTSTR mszStrings) const
{
  int n;
  for (n = 0; mszStrings[0] != 0; n++)
  mszStrings = _tcschr(mszStrings, 0) + 1;
  return(n);
}

Podsumowanie

Przenoszenie programu Spy++ z oryginalnego kodu Visual C++ 6.0 do najnowszego kompilatora trwało około 20 godzin czasu kodowania w ciągu około tygodnia. Uaktualnialiśmy bezpośrednio przez osiem wersji produktu z programu Visual Studio 6.0 do programu Visual Studio 2015. Jest to teraz zalecane podejście do wszystkich uaktualnień w projektach dużych i małych.

Zobacz też

Przenoszenie i uaktualnianie: Przykłady i analizy przypadków
Poprzednie badanie przypadku: COM Spy