Meu caso de uso é o seguinte para um projeto:
- Arquivo de cabeçalho AC com algumas funções externas declaradas
- Um arquivo de origem C++ com essas funções definidas
- Compilar o código fonte C++ em uma biblioteca compartilhada
- Em um arquivo de origem C, use as funções declaradas no cabeçalho C vinculando-as ao .so
Estou observando algo estranho enquanto faço do. As coisas começam a funcionar com namepsace sem nome.
Aqui está meu arquivo de exemplo:
c_sample.h
:
#include "stddef.h"
extern void hello(void);
extern void bye(void);
cpp_sample.cc
:
#include <iostream>
#include "c_sample.h"
extern "C" {
void hello(void) { std::cout << "HI" << std::endl; }
void bye(void) { std::cout << "BYE" << std::endl; }
}
Ao tentar construir uma biblioteca de compartilhamento, vejo o erro que é esperado, pois c_sample.h
está incluído fora do extern "C"
bloco.
g++ cpp_sample.cc -shared -o libcppsample.so
cpp_sample.cc:5:7: error: declaration of 'hello' has a different language linkage
5 | void hello() { std::cout << "HI" << std::endl;}
| ^
./c_sample.h:3:13: note: previous declaration is here
3 | extern void hello();
| ^
cpp_sample.cc:6:7: error: declaration of 'bye' has a different language linkage
6 | void bye() { std::cout << "BYE" << std::endl;}
| ^
./c_sample.h:4:13: note: previous declaration is here
4 | extern void bye();
| ^
2 errors generated.
No entanto, a mágica acontece no momento em que envolvo isso em um namespace sem nome
cpp_sample.cc
:
#include <iostream>
#include "c_sample.h"
extern "C" {
namespace {
void hello(void) { std::cout << "HI" << std::endl; }
void bye(void) { std::cout << "BYE" << std::endl; }
}
}
Isso foi compilado. E quando tentei usá-lo de outro arquivo de origem C, ele até funcionou
#include "stdio.h"
#include "c_sample.h"
int main() {
hello();
}
$ gcc another.c -L/tmp -lcppsample -o another
$ ./another
HI
Como isso funciona apenas envolvendo-o dentro de um namespace? Como ele é capaz de vincular as funções declaradas com suas definições?
Aqui está uma reprodução trivialmente diferente do seu caso de compilação com falha:
Eu apenas removi
#include "stddef.h"
dec_sample.h
, pois ele não contribui em nada, e a falha de compilação é o que você espera e entende:Agora aqui está um código fonte C++ ligeiramente diferente da sua
cpp_sample.cc
versão #2 compilada com sucesso.A diferença aqui é que eu defini
bye
em um namespace diferente (ns
), do namespace (<anonymous>
) que contém a definição dehello
. Isso é só para eliminar qualquer diferença que faça. Mas não faz nenhuma:Ele compila limpo. A tabela de símbolos do arquivo de objeto de saída é:
onde podemos ver que os nomes das funções de origem
void <anonymous>::hello(void)
evoid ns::bye(void)
, foram traduzidos para símbolos de linker globais definidoshello
ebye
sem a manipulação de nomes em C++, exatamente como exigido peloextern "C"
escopo em que são declarados e definidos: nenhum namespace e nenhuma assinatura de chamada são codificados nos símbolos de linker. Eles são apenas símbolos C antigos e simples, ao contrário de, digamos:E assim:
está bem.
extern "C"
não altera a análise sintática do compilador da gramática C++. Ao incluir as definições de função dehello
ebye
em um namespace ou namespaces diferentes, os identificadores são distinguidos das declarações incluídas dec_sample.h
, como sempre seriam. Masextern "C"
suprime a confusão de nomes C++ dos símbolos que são emitidos no código do objeto para os identificadores incluídos no namespace em favor da simbolização C. Então, os namespaces são perdidos, e as assinaturas de chamada. Tudo o que importa para o vinculador é que ele possa encontrar definições dos símbolos referenciados nos arquivos de objeto e bibliotecas que são de entrada,hello
,bye
,_ZSt4cout
etc.Podemos enfatizar essa distinção com:
onde
void <anonymous>::hello(void)
evoid ns::bye(void)
são definidos duas vezes, uma vez dentro do escopoextern "C"
e outra não.O
extern "C"
escopo não faz diferença para a análise sintática de C++: as múltiplas definições são C++ ilegais. Não chegamos nem a emitir símbolos, C++-mangled ou não. Mas se eles não fossem ilegais e chegássemos tão longe, os símbolos emitidos do escopo de ligação C++ padrão seriam:e os emitidos pelo
extern "C"
osciloscópio seriam como antes:sem múltiplas definições no momento do link.
Você pode ver que
good.cc
é equivalente a:onde é óbvio que as declarações:
com ligação C++ por padrão são simplesmente não relacionados às definições incluídas no namespace e são redundantes. Assim, com outro arquivo de origem C++:
podemos compilar e vincular outro programa:
em que não são redundantes. Aqui, compilar com
g++
,main.c
é compilado como fonte C++ (isso me poupa de copiar o mesmo arquivo com.cc
extensão) e as declarações dec_sample.h
têm ligação C++ por padrão: ou seja, elas produzirão símbolos C++-mangled. Então vemos que:main.o
faz referências externas aos símbolos C++-mangled_Z5hellov
e_Z3byev
, que são desmangled:e são definidos em:
A tabela de símbolos deste programa:
tem os
extern "C"
símbolos vinculados degood.o
e os símbolos C++ vinculados dehellobye.o
, mas os símbolos degood.o
são redundantes:Da mesma forma para o programa:
onde os símbolos
hellobye.o
são redundantes.Você pergunta especificamente sobre
g++ -std=c++11
, mas você fará as mesmas descobertas com recenteclang/clang++
ou MSVC, padrão padrão. Ao contrário deg++
,clang++
avisa quando um*.c
arquivo é inserido, mas comog++
compila como fonte C++. MSVC requer a opção/TP
de fazê-lo compilar um*.c
arquivo como C++.O que você queria alcançar era escrever uma biblioteca compartilhada em C++ que tivesse uma API C. Veja como você faria isso:
Crie a biblioteca
Vincule-o a um programa em C:
Ou vincule-o a um programa C++, onde a API C será ativada por meio do arquivo de cabeçalho:
Quando você
#include "c_sample.h"
está em um arquivo C++, você declara algumas funções com vinculação C++ .Quando você declara funções manualmente no namespace global, você obtém um conflito. Uma função com ligação C não pode ter o mesmo nome que uma função com ligação C++ no mesmo namespace.
Quando você declara funções manualmente no namespace sem nome, não há conflito. Você pode declarar uma função em qualquer namespace e uma função com o mesmo nome em um namespace diferente, independentemente da vinculação.
A função de ligação AC declarada em qualquer namespace é apenas uma função C comum que pode ser implementada em um arquivo de origem C e compilada por um compilador C (se nenhum tipo específico de C++ estiver envolvido).
Não há nada indefinido ou específico de implementação aqui, mas o código é frágil e não muito útil. A coisa certa a fazer é adicionar contitionally-compiled
extern "C"
bem no cabeçalho C.