Introdução
A linguagem AdvPL permite a execução de aplicações externas. E este recurso é muito interessante quando precisamos fazer automação de processos e integrações com sistemas legados ou aplicações externas ao TOTVS Application Server. Nesta série de tópicos, vamos abordar o que o AdvPL nos oferece para realizar estas tarefas, e para abrir esse assunto, este tópico têm um exemplo funcional de DLL para ser usada no SmartClient, que permite capturar a tela da máquina que está executando o SmartClient.
Visão Geral de Aplicações Externas
Podemos definir como “aplicações externas”, qualquer programa executável, com ou sem interface, criado para uma determinada finalidade, que não pertence ao conjunto de ferramentas do Protheus.
Existem inúmeras aplicações externas que o Protheus realiza algum tipo de integração. Por exemplo, um servidor de e-mail ou de FTP. É uma aplicação externa, que pode ser acessada através de uma conexão TCP, onde já existe uma função ou classe em AdvPL para encapsular as funcionalidades desta aplicação, como as classes de FTP e e-MAIL.
Normalmente muitas aplicações externas são sistemas complexos, com múltiplas funcionalidades. Algumas delas oferecem APIS de acesso via rede TCP/IP, mediante troca de mensagens em formato específico ou proprietário, ou usando XML-SOAP sobre HTTP, REST sobre HTTP, etc.
Aplicações Específicas ou Especialistas
Existem alguns tipos de aplicações, normalmente menores e criadas para tarefas especializadas, que não oferecem uma camada de acesso remoto, ou mesmo alguma API, mas sim apenas uma DLL ou um executável sem interface, onde devemos realizar a chamada da operação desejada através do endereço de uma função publicada na DLL, ou mesmo através da linha de comando do sistema operacional. Nesta abordagem de aplicações externas, vamos focar inicialmente nestes casos.
Recursos do AdvPL
Usando determinadas funções do AdvPL, podemos realizar a carga de uma DLL, e a chamada de uma função publicada nesta DLL, desde que a mesma atenda a especificação de parâmetros estabelecida, e podemos chamar programas externos (executáveis ou mesmo comandos do sistema operacional).
Porém, devido a uma (importante) questão de segurança e isolamento, ñenhum destes recursos está disponivel para carga de dlls ou execução de programas na instância do TOTVS | Application Server (serve-side). Elas foram feitas para trabalhar em conjunto com o TOTVS SmartClient.
Desse modo, a sua aplicação AdvPL precisa ser iniciada a partir de um SmarrtClient, para a partir dele carregar uma DLL ou chamar um programa executável disponivel na máquina onde o Smartclient está sendo executado. Deste modo, nenhum processo executado em JOB dentro do Protheus Server consegue carregar uma DLL no servidor, ou iniciar um aplicativo executável externo.
SmartClient – Carga de DLL
Existem 5 (cinco) funções no AdvPL que lidam com a carga e execução de funções em DLL, a seguir :
ExecInDLLOpen() – Carrega a DLL na área de memória do SmartClient
ExecInDLLRun() – Executa uma função pré-definida na DLL carregada
ExeDLLRun2() – Executa uma função pré-definida na DLL carregada
ExeDLLRun3() – Executa uma função pré-definida e diferenciada na DLL carregada
ExecInDLLClose()- Descarrega a DLL da memória do SmartClient
A DLL criada deve conter uma (e apenas uma) das prototipagens abaixo, para ser chamada pelo AdvPL:
extern “C” __declspec(dllexport) void ExecInClientDLL(int , char * , char * , int )
extern “C” __declspec(dllexport) int ExecInClientDLL(int , char * , int , char * , int )
Usamos a primeira prototipagem para realizar chamadas pelas funções AdvPL ExecInDLLRun() e ExeDLLRun2(), e a segunda pode ser chamada apenas pela ExeDLLRun3(). Devemos escolher apenas uma prototipagem a ser usada. Não é possível usar as duas na mesma DLL. Você pode criar uma DLL usando C ou C++, ou Delphi, ou outra linguagem que permita a declaração da função na prototipagem adequada. Quando precisamos encapsular um ou mais funcionalidades dentro de um projeto que dependem de DLLs de terceiros, podemos encapsular a DLL de terceiros, construindo um projeto de uma DLL que faça link dinâmico com a DLL de terceiros a ser encapsulada, fazendo a camada de chamadas dentro da função prototipada.
Uma aplicação externa em uma DLL pode abrir uma interface própria, sobre a interface do SmartClient, ou pode ser usada simplesmente para acessar um dispositivo externo ou realizar qualquer outra tarefa sem interface. Existem limitações sobre os parâmetros e retorno da chamada, estes limites variam de acordo com a função AdvPL utilizada para realizar a chamada.
Parâmetros e Retornos
Basicamente, todas as prototipagens permitem a passagem simultânea de um parâmetro numérico inteiro e uma string, e um retorno de uma string. A diferença entre elas está no tamanho dos parâmetros, e a última prototipagem permite também o retorno simultâneo para o AdvPL de um valor numérico e uma string. A documentação da TDN aborda em detalhes o funcionamento esperado de cada uma, vamos focar aqui em um exemplo prático.
DLL de Captura de Tela
Para mostrar como a carga e execução de uma DLL no TOTVS SmartClient funciona, vamos começar com um projeto em C++, que pode ser compilado a partir do Visual Studio 2005 e versões superiores. Basta criar um “Empty Project” em C++ no Visual Studio, definir que o projeto será uma DLL, e criar o fonte de cabeçalho “GetScreen.hpp” e “GetScreen.cpp”, com os conteúdos abaixo:
Arquivo [getscreen.hpp]
void DoCapture( char * file , char * result ); void ScreenCapture(int x, int y, int width, int height, char *filename, char * result );
Arquivo [getscreen.cpp]
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <windows.h> #include <gdiplus.h> #include <memory> #include "GetScreen.hpp" /* ------------------------------------------------------------ Programa GetScreen Autor Júlio Wittwer Data 07/09/2016 Descrição DLL Win32 para uso com o TOTVS SmartClient Permite a captura da tela do computador onde o SmartClient está sendo executado gerada como uma imagem .JPEG salva em disco, usando o nome do arquivo fornecido como parâmetro. Utilização Utilizar a função ExecInClientDLL(), informando os seguintes parâmetros: int ID => Número da operação desejada 0 = Obter a versão da API 1 = Capturar um ScreenShot em formato JPEG char * BufIn => array de char contendo um parâmetro para a opção. SE ID = 0, o parâmetro é ignorado e pode ser NULL SE ID = 1, deve ser informado o nome do arquivo JPEG no qual a captura de tela deve ser gravada. char * BufOut => Array de char contendo o resultado da chamada, no formato NNN XXXXXXXX, onde NNN é um Código de Status , e XXXXX contém uma informação adicional sobre o status retornado. Em caso de sucesso, o número retornado é "000" Qualquer outro número indica uma condição de falha. "001 Encode size failed" "002 Memory allocation failed" "003 Image Codec not found" "004 Image Save Error (%d)" "005 Unexpected Error %d "010 Unknow Command" ------------------------------------------------------------ */ extern "C" __declspec(dllexport) void ExecInClientDLL( int ID, char * BufIn, char * BufOut, int nSizeOut ) { if( ID == 0 ) { // Retorna a versão da DLL de captura strcpy(BufOut,"000 GetScreen V 0.160911"); } else if (ID == 1) { // REaliza a captura da tela // Recebe em BuffIn o nome do arquivo a ser salvo // Retona em buffOut o status da operação // Em caso de sucesso, retorno "000 Ok" // Em caso de erro, retorno "NNN <error message>" DoCapture(BufIn,BufOut); } else { // ID não conhecido/inválido strcpy(BufOut,"010 Unknow Command"); } } using namespace Gdiplus; using namespace std; // Inicializa GDI para a captura de vídeo // faz a captura, salva em disco, e finaliza GDI void DoCapture( char * file , char * result ) { // Initialize GDI+. GdiplusStartupInput gdiplusStartupInput; ULONG_PTR gdiplusToken; GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL); int x1 = 0; int y1 = 0; int x2 = GetSystemMetrics(SM_CXSCREEN); int y2 = GetSystemMetrics(SM_CYSCREEN); // Realiza a captura da tela e salva em arquivo ScreenCapture(x1, y1, x2 - x1, y2 - y1, file , result ); // Shutdown GDI+ GdiplusShutdown(gdiplusToken); } // Retorna o ponteiro do encoder adequado para fazer // a conversão do BITMAP em memória para o formato desejado int GetEncoderClsid(const WCHAR* format, CLSID* pClsid ) { UINT num = 0; // number of image encoders UINT size = 0; // size of the image encoder array in bytes ImageCodecInfo* pImageCodecInfo = NULL; GetImageEncodersSize(&num, &size); if(size == 0) { // Encode Size Failure return -1; } pImageCodecInfo = (ImageCodecInfo*)(malloc(size)); if(pImageCodecInfo == NULL) { // Memory allocation failure return -2; } GetImageEncoders(num, size, pImageCodecInfo); for(UINT j = 0; j < num; ++j) { if( wcscmp(pImageCodecInfo[j].MimeType, format) == 0 ) { *pClsid = pImageCodecInfo[j].Clsid; free(pImageCodecInfo); // Success return j; } } free(pImageCodecInfo); // Image Codec not found return -3; } // Encapsula a gravação do BITMAP capturado da tela // para o formato JPEG em disco void BitmapToJpg(HBITMAP hbmpImage, char *filename , char * result ) { Status eRC = Ok; Bitmap * p_bmp = Bitmap::FromHBITMAP(hbmpImage, NULL); CLSID pngClsid; int RC = GetEncoderClsid(L"image/jpeg", &pngClsid); if( RC >= 0 ) { const size_t cSize = strlen(filename)+1; std::wstring wc( cSize, L'#' ); mbstowcs( &wc[0], filename, cSize ); eRC = p_bmp->Save(&wc[0], &pngClsid, NULL); if ( eRC != Ok) RC = -4; } delete p_bmp; if ( RC == -1 ) sprintf_s(result,255,"001 Encode size failed"); else if ( RC == -2 ) sprintf_s(result,255,"002 Memory allocation failed"); else if ( RC == -3 ) sprintf_s(result,255,"003 Image Codec not found"); else if ( RC == -4 ) sprintf_s(result,255,"004 Image Save Error (%d)",eRC); else if ( RC < 0 ) sprintf_s(result,255,"005 Unexpected Error %d",RC); else sprintf_s(result,255,"000 Ok"); } // Funão de captura / snapshot de tela // Requer o ambiente DGI previamente inicializado void ScreenCapture(int x, int y, int width, int height, char *filename, char * result ) { HDC hDcMemory = CreateCompatibleDC(0); HDC hDcScreen = GetDC(0); HBITMAP hBmp = CreateCompatibleBitmap(hDcScreen, width, height); SelectObject(hDcMemory, hBmp); BitBlt(hDcMemory, 0, 0, width, height, hDcScreen, x, y, SRCCOPY); // Converte a tela capturada em JPEG e salva em disco BitmapToJpg(hBmp, filename , result ); // Cleanup DeleteObject(hBmp); DeleteObject(hDcMemory); ReleaseDC(NULL,hDcScreen); }
Feito isso, basta fazer a “build do Projeto, gerando uma DLL 32 bits, e copiá-la para a pasta do TOTVS SmartClient.
Agora, crie um fonte AdvPL, chamado por exemplo de “GetScreen.PRW”, adicione-o a um projeto no IDE/TDS, com o seguinte conteúdo:
Arquivo [GetScreen.PRW]
/* ------------------------------------------------------------ Funcao U_GetSCR() Autor Júlio Wittwer Data 09/2016 Descrição Tira um ScreenShot da tela da máquina onde está sendo executado o TOTVS | SmartClient, e copia o JPEG com a imagem gerada para uma pasta chamada "\images\" a partir do RootPath do ambiente no Servidor Observação A Pasta \images\ a partir do RootPath deve ser criada antes de executar este programa ------------------------------------------------------------ */ User function GetSCR() Local hHdl := 0 Local cRet Local nCode Local cRmtFile Local cSrvFile Local cImgFile // Abre a DlL hHdl := ExecInDLLOpen( "GetScreen.dll" ) IF hHdl == -1 UserException("Failed to load [GetScreen.dll]") Endif // Pega a versao do GetScreen cRet := ExecInDllRun( hHdl, 0, "" ) conout(cRet) // identifica a pasta temporaria da maqina // que está rodando o smartclient // e usa ela pra criar a imagem do screenshot cImgFile := 'img_'+cValTochar(Threadid())+'.jpeg' cRmtFile := GETTEMPPATH() + cImgFile; Msgrun( "Aguarde",; "Executando captura de tela",; {|| cRet := ExecInDllRun( hHdl, 1, cRmtFile )} ) If empty(cRet) UserException("Unexpected Empty result from [GetScreen.dll]") Endif // Verifica o codigo retornado // 000 = Sucesso // <> 0 = Falha nCode := val(left(cRet,3)) If nCode > 0 UserException(cRet) Endif // copia o arquivo do terminal para o servidor CPYT2S(cRmtFile,"\images\") // Apaga o arquivo na pasta temporária da estação Ferase(cRmtFile) // Informa a operação realizada MsgInfo("Imagem salva no servidor em \images\"+cImgFile) // ---------------------------------------------------------------- // Fecha a DLL ExecInDllClose( hHdl ) Return
Execução
Após gerar a DLL 32 bits no Visual Studio, copiá-la para a pasta do SmartClient, e compilar o fonte AdvPL “GetScreen.PRW”, e criada uma pasta chamada “\images\” a partir do RootPath do seu ambiente do Protheus, basta iniciar um Totvs SmartClient, e executar o programa “U_GetSCR”. Ele mostrará rapidamente uma caixa de diálogo da função MsgRun(), a partir de onde será feita a captura da tela na estação onde está sendo executado o TOTVS SmartClient, e logo depois uma caixa de diálogo, informando o nome do arquivo que contém a imagem capturada na pasta \images\ no ambiente do Protheus Server.
O Fonte em AdvPL determina qual é o nome da pasta temporária da máquina onde o SmartClient está sendo executado, cria um nome para salvar a imagem, chama a DLL pra fazer a captura da imagem, informando o nome do arquivo onde a imagem deve ser salva, e depois apenas copia o arquivo gerado no disco da estação onde o SmartClient está sendo executado para a pasta “\images\” do Protheus Server, e apaga o arquivo temporário da estação.
Estendendo as funcionalidades
O modelo proposto permite que você implemente literalmente milhares de instruções dentro de uma DLL, mesmo tendo apenas uma função prototipada, pois podemos criar uma funcionalidade com tratamento diferenciado de parâmetros e retorno para cada nID utilizado. Usando o ID 0 (zero) para controlar a versão da API, quando você implementar uma funcionalidade nova nesta DLL, basta atualizar o número da versão, e no seu programa em Advpl, basta verificar qual é a versão em uso, para saber se ela já possui a funcionalidade desejada. Por exemplo, baseado neste programa, você pode implementar o ID=2 para pegar a quantidade de monitores que existe na máquina Client, ou a resolução atual de tela, ou qualquer outra coisa que lhe seja útil.
Fontes e DLL
Para você que quer compilar, testar e usar este recurso, ou estendê-lo, basta pegar os fontes no https://github.com/siga0984/Blog — todos os arquivos que começam com “GetScreen.*”. Inclusive, também disponibilizei o arquivo getscreen.dll, compilado para Windows 32 Bits. Basta copiar ele para a pasta do SmartClient, compilar o programa AdvPL GetScreen.PRW no repositório do ambiente Protheus e utilizá-lo. Segue abaixo links do GitHub para cada um deles.
https://github.com/siga0984/Blog/blob/master/GetScreen.hpp
https://github.com/siga0984/Blog/blob/master/GetScreen.cpp
https://github.com/siga0984/Blog/blob/master/GetScreen.prw
https://github.com/siga0984/Blog/blob/master/GetScreen.dll
Conclusão
Esse é um exemplo bem arroz-com-feijão, apenas pra dar um gostinho do que podemos fazer com esta integração, e ainda nem chegamos na camada de chamar aplicações externas — que também é uma “mão na roda”. No próximo post, vou entrar um pouco mais em alguns aspectos e propriedades desta integração com DLL, que abrem um amplo leque de possibilidades !!!
Agradeço a todos(as) pela audiência, e lhes desejo TERABYTES de SUCESSO 😀
Informações Adicionais
RELEASE 2016/09/14 23:32 – Meus agradecimentos ao meu chapa Pedro Scarapicchia, por localizar e me apontar 3 pontos de leak na função ScreenCapture() 😀 Post e GitHub atualizados 😉 Pedro, obrigado por me lembrar das boas práticas — a pressa de colocar o post no ar foi maior que a atenção no código ..rs…
Referências
http://tdn.totvs.com/display/tec/ExecInDLLOpen
http://tdn.totvs.com/display/tec/ExecInDLLRun
http://tdn.totvs.com/display/tec/ExeDLLRun2
http://tdn.totvs.com/display/tec/ExeDLLRun3
http://tdn.totvs.com/display/tec/ExecInDLLClose
Mais um excelente post, vlw Júlio!!!
Já baixei os códigos e compilei pra testar, funcionou!!!
Agora fiquei ansioso pra saber mais sobre isso e vou aguardar os próximos posts…
Algumas dúvidas/sugestões para o próximo post:
Toda DLL possui uma documentação?
Como posso saber quais funções usar de uma DLL?
Gostaria de ver alguns exemplos com dlls já consagradas:
Ex:
7z.dll pra compactar e descompactar arquivos
gsdll.dll = ghostscript = impressão em PDF e outros formatos
Alguma para trabalhar com QRCode
Sou iniciante e não sabia nada de manipulação de DLL até este post.
Agora já sei algumas coisas e vou aprender mais nos próximos posts 🙂
Só uma pequena correção no texto:
memporia = memória
CurtirCurtido por 1 pessoa
Opa, perfeitamente !!! Suas sugestões são pertinentes, acho que não dá pra atender todas de uma vez, mas vou tentar trazer algumas delas no próximo post 😀
Abraços 😉
CurtirCurtir
Mais um ótimo post.
Uma dúvida, Julio, como faz para acessar uma lib no Linux (.so), estas mesmas funções servem ?
CurtirCurtido por 1 pessoa
Olá Enderson, obrigado !! Sim, são as mesmas funções 😀
CurtirCurtir
Júlio, bom dia!
Seguindo seus exemplos, foi tranquilo fazer a dll,, mas quando executo a dll em uma máquina sem o Visual Studio a o Protheus não reconhece o arquivo.
Terias alguma dica?
Grato desde já!
CurtirCurtido por 1 pessoa
Opa, perfeitamente !!! Dependendo dos recursos utilizados no projeto, a sua DLL pode ter dependências do Visual Studio Redistributable Package. Experimenta baixar a versão do Visual Studio Redistributable Package na máquina em questão 😉
Abraços
CurtirCurtir