Pular para o conteúdo principal

C, C++ e string: Uma combinação (quase) perfeita

As linguagens de programação C e C++ possuem uma relação. Praticamente vivem em harmonia e sintonia – mesmo existindo compiladores e padrões distintos. Essa combinação é que possibilita escrevermos abstrações “próximo ao metal”, estabelecendo um equilíbrio entre a compreensão (do código, que depende de ponto de vista e conhecimento) e o desempenho.

O problema é quando truques e hacks entram em ação. Um dos meus favoritos com C++ é sobrescrever no buffer de uma string. Isso é feito através do retorno do método c_str(), que disponibiliza um ponteiro para este buffer interno. No entanto, é necessário fazer um const_cast para remover o constness do ponteiro e assim sobrescreve-lo.

Os exemplos que elaborei para este post são uma tentativa de simular um problema real que me custou quase um dia inteiro de depuração, por causa de um hacking “metido a besta”. :-)



Sabemos que a maioria dos sistemas operacionais, por exemplo, Windows ou Linux, fornecem APIs em C para uma parte significativa de seus subsistemas. Até mesmo algumas funções da biblioteca padrão do C são necessárias por não existir no C++ ou simplesmente por estarmos familiarizados de longa data com elas. E acaba sendo natural utilizá-las (usar diretamente) ou encapsulá-las (usar indiretamente).

Agora, onde uma string (por exemplo, std::string ou std::wstring) entra nisso tudo? Ok, vamos lá.

Algumas APIs ou funções C lidam com ponteiros de caracteres (char* ou wchar_t*), e é neste ponteiro que devemos informar um buffer. Por exemplo, API do Windows GetUserName, sobrescreve um array de caracteres com o nome do usuário que está no contexto da execução:

[code language="cpp"]
wchar_t buffer[64];
DWORD size = sizeof(buffer) / sizeof(buffer[0]);
if (GetUserName(buffer, &size))
return buffer;
[/code]

Note que ela recebe no segundo argumento atual o endereço de uma variável contendo o tamanho de caracteres do buffer (fique atento pois algumas funções podem receber o tamanho em bytes), ela será sobrescrita com a quantidade de caracteres escrito no buffer.

Estando no C++, imagine encapsular está funcionalidade numa função chamada username. Uma assinatura natural para esta função é:

[code language="cpp"]
std::wstring username()
[/code]

Onde, ela retorna o conteúdo numa std::wstring. E a implementação completa pode ser:

[code language="cpp"]
std::wstring username()
{
wchar_t buffer[64];
DWORD size = sizeof(buffer) / sizeof(buffer[0]);
if (GetUserName(buffer, &size))
return buffer; //converted implicitly to std::wstring
throw std::runtime_error("GetUserName failed");
}
[/code]

Podendo ser consumida da seguinte forma:

[code language="cpp"]
const std::wstring& s = username();
std::wcout << s << "\n";
std::cout << hex_rep(buffer_to_transmit(s)) << "\n";
[/code]

Produzindo um possível resultado do meu usuário e uma representação do buffer em hexadecimal, que supostamente seria transmitido através de algum meio de comunicação:

fgaluppo
Size: 16 bytes
6600670061006C007500700070006F00

Ao olharmos a função username, notaremos que o buffer retornado é uma cópia convertida implicitamente para uma std::wstring. O ideal seria evitar a cópia temporária e retorná-la, aproveitando os benefícios do move constructor. Ai é onde o nosso hacking entrará. Ao invés de criar um array de wchar_t, porque não alocar uma std::wstring e passar seu buffer para ser sobrescrito? Abaixo um ajuste do código para suportar esta idéia:

[code language="cpp"]
std::wstring username2()
{
std::wstring buffer;
buffer.resize(64);
DWORD size = buffer.size();
wchar_t* buffer_ptr = const_cast<wchar_t*>(buffer.c_str());
if (GetUserName(buffer_ptr, &size))
return buffer;
throw std::runtime_error("GetUserName failed");
}
[/code]

Ou seja, é passado para a função GetUserName um ponteiro válido com conteúdo mutável, obtido através de:

[code language="cpp"]
wchar_t* buffer_ptr = const_cast<wchar_t*>(buffer.c_str());
[/code]

Se imprimirmos no console (ou redirecionarmos a stream de saída) com std::wcout com o retorno da função username2, o resultado será o mesmo:

fgaluppo

E tudo parece ok. O parecer correto se dá por causa que ao final, o conteúdo do buffer, termina com ‘\0’ - wchar_t com 2 bytes adjacentes ‘\0’ e ‘\0’. Isso é o suficiente para o std::wcout imprimir uma string corretamente. No entanto, o tamanho da string mantido pelo std::wstring (ou std::string) pode ser diferente, no caso do exemplo, é 64 – conforme informado em resize. Este tamanho não foi sincronizado com o buffer sobrescrito. É ai que um bug fica exposto, veja a representação do hexadecimal do buffer a ser transmitido:

Size: 128 bytes
6600670061006C007500700070006F0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

Exceto pelos bytes iniciais, ele é diferente da versão original que tem apenas 16 bytes para serem transmitidos!

O problema foi exposto pela função buffer_to_transmit, que considera a string e o seu membro do tipo função size – o que faz sentido afinal é um membro que faz parte do objeto, e ele devia estar consistente:

[code language="cpp"]
std::vector<char> buffer_to_transmit(const std::wstring& s)
{
std::vector<char> temp;
size_t N = 2 * s.size();
temp.resize(N);
const char* ptr = reinterpret_cast<const char*>(s.c_str());
std::copy(ptr, ptr + N, temp.begin());
return std::move(temp);
}
[/code]

O que aconteceu aqui foi:

  • A função username2 retornou uma std::wstring que previamente tinha o tamanho de 64 caracteres (ou 128 bytes, pois o sizeof de wchar_t é 2);

  • Ocorreu um const_cast para sobrescrever o buffer interno desta string;

  • O conteúdo retornado era menor do que o alocado previamente;

  • Não houve uma sincronização do novo tamanho.


Logo, o conteúdo do buffer interno, não estava refletindo corretamente com o size, ou vice-versa. Aliás, isto pode ser visto como uma feature (um comportamento esperado) ou como um bug. No entanto, corrigir isto me parece trivial, pois a API GetUserName retorna a quantidade de caracteres sobrescritos. Então, uma implementação correta é:

[code language="cpp"]
std::wstring username3()
{
std::wstring buffer;
buffer.resize(64);
DWORD size = buffer.size();
wchar_t* buffer_ptr = const_cast<wchar_t*>(buffer.c_str());
if (GetUserName(buffer_ptr, &size))
{
--size; //The second argument of GetUserName computes the '\0'
buffer.resize(size); //The (w)string resize performs the actual size + 1
return buffer;
}
throw std::runtime_error(&GetUserName failed&);
}
[/code]

Ou seja, efetuar o resize novamente com a quantidade de caracteres sobrescritos. Agora pareceu fácil e sem problemas! :-)

Existem APIs que não retornam a quantidade de caracteres ou bytes escritos no buffer. Por exemplo, a API do C, para formatação de data e horário, strftime:

[code language="cpp"]
tm* t = localtime(&now);
strftime(buffer, buffer.size(), &%Y-%m-%d %T&, t);
[/code]

Neste caso, para encurtarmos caminho, é só executar a função strlen com o buffer interno da string (ou c_str – afinal isto quer dizer C string) dessincronizada. Uma implementação correta encapsulando esta função é:

[code language="cpp"]
std::string timestamp3()
{
time_t now = time(nullptr);
tm* t = localtime(&now);
if (nullptr == t)
throw std::runtime_error(&localtime failed&);
std::string buffer(64, ' ');
char* buffer_ptr = const_cast<char*>(buffer.c_str());
if (0 == strftime(buffer_ptr, buffer.size(), &%Y-%m-%d %T&, t))
throw std::runtime_error(&strftime failed&);
std::string temp(buffer_ptr, std::strlen(buffer_ptr)); //converted explicitly to std::string using ptr and len
return temp;
}
[/code]

Se executarmos as três versões desta forma:

[code language="cpp"]
for (const std::string& s : { timestamp(), timestamp2(), timestamp3() })
{
std::cout << s << &\n&;
std::cout << hex_rep(buffer_to_transmit(s)) << &\n&;
}
[/code]

Obteremos uma saída similar há:

2016-05-10 19:16:31
Size: 19 bytes
323031362D30352D31302031393A31363A3331
2016-05-10 19:16:31
Size: 64 bytes
323031362D30352D31302031393A31363A3331002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020
2016-05-10 19:16:31
Size: 19 bytes
323031362D30352D31302031393A31363A3331

Onde as versões 1 e 3 serão os resultados esperados.

Abaixo uma compilação e execução do exemplo timestamp:

compile_and_run_timestamp

Fontes:
https://github.com/SimplyCpp/examples/tree/master/c_cpp_string

Comentários

Postagens mais visitadas deste blog

Mestre Iota

Iota é a nona letra do alfabeto grego, ela é equivalente à letra i do nosso alfabeto. Por convenção ou hábito, utilizamos a letra i na programação para indicar algum tipo de incrementador, como por exemplo, em um for-loop . https://gist.github.com/fabiogaluppo/a23894ae743f7dd29274 Curiosamente, iota , como identificador, também é utilizado na programação para indicar uma sequência finita e consecutiva de números inteiros, como por exemplo, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]. Inclusive, originalmente na STL existia a função iota , inspirada pela linguagem de programação APL , você pode conferir neste link: http://www.sgi.com/tech/stl/iota.html

Valores Aleatórios Simplificados

A partir do C++ 11, foi introduzido o header <random>  com diversos facilitadores para suporte de geração de números aleatórios. A produção destes números é feita através da combinação de duas categorias de objetos: os geradores e os distribuidores. Os geradores, são responsáveis pela geração dos números, e os distribuidores são responsáveis pela transformação dos números gerados em algum tipo de distribuição de probabilidade.  Como por exemplo, uma distribuição normal (aquela da Gaussiana) ou uma distribuição de Pareto (aquela do 80-20). As opções não faltam, como você pode ver nas referências, por exemplo:   http://www.cplusplus.com/reference/random/ ou  http://en.cppreference.com/w/cpp/header/random .

Policy-based design: log writer

Policy-based design Vamos neste artigo dar mais uma pincelada no Policy-based design . Vamos fazer como exemplo uma classe de log. Como este é só um exemplo, não vamos considerar múltiplos parâmetros no log, mas somente uma string, assim não fugiremos do assunto. Uma das coisas mais importantes neste tipo de design é o desacoplamento. Ele é uma excelente alternativa ao uso de interfaces por duas razões: Não gera chamadas virtuais (ou um nível de indireção em tempo de execução) Duck typing ( https://pt.wikipedia.org/wiki/Duck_typing ) Eu gosto bastante desse tipo de design, já usado aqui: http://simplycpp.com/2016/02/05/leitura-de-configuracao-em-c/