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:

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

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 é:

std::wstring username()

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

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");
}

Podendo ser consumida da seguinte forma:

const std::wstring& s = username();
std::wcout << s << "\n";
std::cout << hex_rep(buffer_to_transmit(s)) << "\n";

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:

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");
}

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

wchar_t* buffer_ptr = const_cast<wchar_t*>(buffer.c_str());

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:

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);
}

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 é:

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&);
}

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:

tm* t = localtime(&now);
strftime(buffer, buffer.size(), &%Y-%m-%d %T&, t);

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 é:

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;
}

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

for (const std::string& s : { timestamp(), timestamp2(), timestamp3() })
{
	std::cout << s << &\n&;
	std::cout << hex_rep(buffer_to_transmit(s)) << &\n&;
}

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

Anúncios

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s