Programação Por Equipe DevBras

Capa do artigo: Chamar DLL nativa em C#: o que cada erro quer dizer

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, CancelarUltimaVenda e outras), e o fabricante não pode criar funções novas.
  • As funções devolvem texto com campos separados por |, codificado em UTF-8. A ConsultarSAT recebe o número de sessão e devolve numeroSessao|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.

Avalie este artigo

Referência: http://tanca.com.br/forum/?topic=como-declarar-a-sat-dll-em-vb-net