Chamar DLL nativa em C#: o que cada erro quer dizer
P/Invoke testado no .NET 10 com a ConsultarSAT: o que DllNotFound, EntryPointNotFound e BadImageFormat apontam, por que o retorno é IntPtr e o UTF-8 do SAT.
Uma declaração DllImport é uma promessa que o compilador não confere: arquivo, nome exportado, arquitetura, convenção de chamada, tipos e dono da memória do retorno. Quando uma dessas partes está errada, o .NET avisa com uma exceção específica ou, no pior caso, derruba o processo. Saber qual exceção corresponde a qual parte resolve a maioria dos problemas com DLL de SAT, TEF, balança e impressora fiscal.
Para mostrar cada caso com a mensagem real, compilamos no box uma biblioteca nativa de teste com a mesma assinatura da ConsultarSAT e a chamamos de um programa .NET 10 (runtime 10.0.14) em C#. O box roda Linux, então a biblioteca é um .so e não uma DLL; onde o Windows se comporta diferente, o texto diz e aponta a documentação da Microsoft.
O que a especificação do SAT define
A Especificação Técnica de Requisitos do SAT (versão ER 2.27.05, publicada pela Secretaria da Fazenda de São Paulo no portal do SAT) fixa três coisas que importam para a declaração:
- Os nomes das funções do driver são padronizados (
ConsultarSAT,EnviarDadosVenda,CancelarUltimaVendae outras), e o fabricante não pode criar funções novas. - As funções devolvem texto com campos separados por
|, codificado em UTF-8. AConsultarSATrecebe o número de sessão e devolvenumeroSessao|EEEEE|mensagem|cod|mensagemSEFAZ, em que 08000 é "SAT em operação.". - O equipamento não pode executar funções em paralelo: uma chamada só é processada depois que a anterior termina.
O que a especificação não fixa é o arquivo da DLL, a arquitetura (32 ou 64 bits) e a convenção de chamada. Isso vem do manual de cada fabricante, e é aí que a declaração copiada de outro projeto costuma errar.
Cada exceção aponta uma parte da declaração
DllNotFoundException: o arquivo, ou uma dependência dele
Declarando uma biblioteca que não está na pasta, o teste devolveu:
DllNotFoundException: Unable to load shared library 'satquenaoexiste' or one of its dependencies.
O "or one of its dependencies" é a parte que mais engana. A DLL do fabricante pode estar na pasta e ainda assim dar essa exceção, porque ela depende de outra DLL (de comunicação USB, do runtime do C++) que não foi copiada. No Windows, a ordem em que o sistema procura cada arquivo está em ordem de pesquisa de DLL; a pasta do executável vem antes das pastas do sistema, então é lá que a DLL e as dependências dela devem ficar. A forma como o .NET monta o nome e os caminhos tentados está em carregamento de bibliotecas nativas.
BadImageFormatException: arquitetura diferente
Para simular a DLL de 32 bits num processo de 64, compilamos a mesma função com gcc -m32. No Linux, o carregador recusa o arquivo e o .NET devolve de novo DllNotFoundException, com o motivo na linha seguinte:
DllNotFoundException: Unable to load shared library 'sat32' or one of its dependencies. ...
.../net10.0/libsat32.so: wrong ELF class: ELFCLASS32
No Windows o mesmo erro aparece como BadImageFormatException. A documentação da exceção descreve exatamente esse caso, um processo de 64 bits chamando uma DLL de 32, e indica fixar o alvo da plataforma em x86 em vez de x64 ou AnyCPU:
<PropertyGroup> <PlatformTarget>x86</PlatformTarget> </PropertyGroup>
Com AnyCPU, o programa roda em 64 bits em qualquer Windows de 64 bits, e a DLL de 32 bits não carrega. A propriedade está na lista de propriedades comuns do MSBuild. Vale gravar no log de inicialização o valor de Environment.Is64BitProcess; o teste imprime Is64BitProcess = True, RID = linux-x64 na primeira linha, e esse dado sozinho já resolve metade dos chamados de suporte.
EntryPointNotFoundException: o nome exportado
A biblioteca de teste exporta ConsultarSAT. Declarando ConsultarSat (só a caixa diferente), o resultado foi:
EntryPointNotFoundException: Unable to find an entry point named 'ConsultarSat' in shared library 'satfake'.
O nome exportado precisa bater letra por letra. Para listar o que a biblioteca publica, o teste usou nm -D --defined-only libsatfake.so, que mostrou T ConsultarSAT. No Windows, a ferramenta é o dumpbin /exports SAT.dll, no Prompt de Comando do Desenvolvedor do Visual Studio, descrito em /EXPORTS. Se a saída mostrar algo como _ConsultarSAT@4, a DLL de 32 bits foi compilada com nomes decorados de __stdcall, e o EntryPoint precisa usar esse texto.
Processo que fecha sem mensagem: tipo ou memória
Declarando o retorno da ConsultarSAT como string, o programa não lançou exceção. Ele imprimiu uma linha da biblioteca C e terminou com código 134 (abortado):
free(): invalid pointer (código de saída: 134)
A explicação está em comportamento padrão do marshalling: quando o retorno é string, o runtime copia o texto e libera a memória original, com CoTaskMemFree no Windows e free nos outros sistemas. Se aquela memória pertence à DLL (um buffer estático, como na biblioteca de teste e em muitos drivers), a liberação corrompe o heap. No Windows o sintoma costuma ser o caixa fechando sem mensagem ou um AccessViolationException em outra chamada, longe da causa.
Texto de volta: IntPtr e UTF-8
A saída é declarar o retorno como IntPtr e converter o texto você mesmo, sem liberar nada que a DLL não mande liberar. Como a especificação define UTF-8, a conversão certa é Marshal.PtrToStringUTF8. O teste leu o mesmo ponteiro dos dois jeitos:
UTF-8: 123456|08000|SAT em operação.|999| Latin-1: 123456|08000|SAT em operação.|999|
A segunda linha é o que aparece no Windows quando se usa Marshal.PtrToStringAnsi, que lê pela página de código ANSI da máquina (em português, a 1252, que coincide com o Latin-1 nesses caracteres). No Linux e no macOS, o .NET trata ANSI como UTF-8, conforme as boas práticas de interoperabilidade nativa, por isso o erro de acentuação só aparece no caixa Windows. Se a mensagem do SAT chega com "ç" no lugar de "ç", é essa a troca a fazer.
Convenção de chamada só pesa em 32 bits
Em 64 bits existe uma convenção só, e o __stdcall é aceito e ignorado, segundo a documentação do __stdcall. Em 32 bits, StdCall e Cdecl discordam sobre quem limpa a pilha, e declarar uma no lugar da outra desalinha a pilha a cada chamada. Como boa parte das DLLs de SAT ainda é de 32 bits, a convenção do manual do fabricante entra na declaração:
[DllImport("SAT.dll", EntryPoint = "ConsultarSAT", CallingConvention = CallingConvention.StdCall)]
internal static extern IntPtr ConsultarSAT(int numeroSessao);
O StdCall acima é um exemplo. Use o que o manual do seu equipamento indicar, e confirme pelo nome no dumpbin: o sufixo @N é marca de __stdcall sem arquivo .def.
Uma chamada por vez
Como o equipamento só processa uma função por vez, o programa não deve chamar a DLL de duas threads ao mesmo tempo (o timer de status e a venda, por exemplo). Uma trava em volta de todas as chamadas resolve:
internal static class Sat
{
private static readonly object trava = new();
public static string Consultar(int numeroSessao)
{
lock (trava)
{
IntPtr ponteiro = SatNativo.ConsultarSAT(numeroSessao);
return Marshal.PtrToStringUTF8(ponteiro) ?? "";
}
}
}
Comece sempre pela ConsultarSAT, que não altera nada. Um erro de declaração testado nela vira uma consulta que falhou; testado na EnviarDadosVenda, pode virar um cupom com dados errados. Como integrar a venda sem duplicar cupom é assunto do guia de SAT fiscal no PDV, e o equivalente para a maquininha está em TEF no caixa sem cobrar duas vezes. Se o PDV atende lojas fora de São Paulo, veja também por que o SAT de SP não se copia para outro estado.
DllImport ou LibraryImport
Desde o .NET 7 existe o LibraryImport, que gera o código de chamada na compilação em vez de em tempo de execução (geração de código para P/Invoke). A mesma função fica assim:
static partial class NativoGerado
{
[LibraryImport("satfake", EntryPoint = "ConsultarSAT")]
internal static partial IntPtr ConsultarSAT(int numeroSessao);
}
No teste, a primeira compilação falhou com SYSLIB1062: LibraryImportAttribute requires unsafe code. O projeto precisa de <AllowUnsafeBlocks>true</AllowUnsafeBlocks> no .csproj, mesmo sem nenhum bloco unsafe escrito à mão. Depois disso, a chamada devolveu 654321|08000|SAT em operação.|999|. Arquitetura, nome, convenção e dono da memória continuam valendo do mesmo jeito; o LibraryImport muda só quem gera o código de marshalling.
A mesma declaração em VB.NET
Imports System.Runtime.InteropServices
Friend Module SatNativo
<DllImport("SAT.dll", EntryPoint:="ConsultarSAT", CallingConvention:=CallingConvention.StdCall)>
Friend Function ConsultarSAT(numeroSessao As Integer) As IntPtr
End Function
End Module
' Dim resposta As String = Marshal.PtrToStringUTF8(SatNativo.ConsultarSAT(numeroSessao))
A regra de memória é a mesma: retorno As String faz o runtime liberar o ponteiro da DLL. Em VB.NET antigo, com Declare Function, o efeito é igual.
O teste
A biblioteca de teste devolve um texto fixo no formato da especificação, num buffer que pertence a ela:
#include <stdio.h>
static char resposta[256];
const char *ConsultarSAT(int numeroSessao) {
snprintf(resposta, sizeof resposta, "%06d|08000|SAT em operação.|999|", numeroSessao);
return resposta;
}
Compilada com gcc -shared -fPIC -o libsatfake.so satfake.c (e a versão de 32 bits com -m32), ela é chamada por um programa console com cinco declarações: a certa, uma com a caixa do nome trocada, uma para um arquivo inexistente, uma para a biblioteca de 32 bits e uma com retorno string. Saída de cada modo:
== certo
UTF-8: 123456|08000|SAT em operação.|999|
Latin-1: 123456|08000|SAT em operação.|999|
LibraryImport: 654321|08000|SAT em operação.|999|
== nome-errado
EntryPointNotFoundException: Unable to find an entry point named 'ConsultarSat' in shared library 'satfake'.
== sem-arquivo
DllNotFoundException: Unable to load shared library 'satquenaoexiste' or one of its dependencies. ...
== 32-bits
DllNotFoundException: Unable to load shared library 'sat32' or one of its dependencies. ...
.../net10.0/libsat32.so: wrong ELF class: ELFCLASS32
== retorno-string
free(): invalid pointer
(código de saída: 134)
O mesmo roteiro serve para a DLL real do seu fabricante no Windows: rode primeiro a declaração certa da ConsultarSAT com o equipamento ligado e guarde a resposta. Se ela vier com 08000, a declaração está certa e as outras funções podem seguir o mesmo padrão.
Referência: http://tanca.com.br/forum/?topic=como-declarar-a-sat-dll-em-vb-net