DE   |   EN   |   RU

Келтенринг 17, D – 82041 Оберхахинг Мюнхен  |  Тел.: +49 89 4554 6533  |  E-Mail: info@intechcore.com 

Применение JNA (Java Native Access) для доступа к «нативным» библиотекам COM-DLL Microsoft Windows.

Применение JNA (Java Native Access) в Java-проектах для доступа к функциональности и объектам, так называемых «нативных» библиотек — COM-DLL Microsoft Windows, представляет собой большой интерес.
Основным преимуществом является сокращение времени разработки проекта, если вся необходимая функциональность уже содержится в какой-то стандартной библиотеке Microsoft Windows, либо есть сторонняя COM-DLL с необходимым набором решений, либо это уже применяемая клиентом COM-DLL бизнес-логики. Также невозможно переоценить возможность использования COM-DLL, работающей с объектами Microsoft .NET Framework, написанная, например, на C#.
Вторым, но не меньшим по значимости преимуществом является то, что в отличие от предыдущей технологии JNI (Java Native Interface), здесь не придется писать библиотеку-оболочку на C, а это и в правду сомнительное удовольствие.
Но обо всем по порядку. Рассмотрим задачу, которая и подвигла на изучение технологии JNA.
Для нужд проекта потребовалось получить список принтеров в системе, но также название их драйверов, т.к. имя принтера можно задать вручную какое угодно, хоть «MySuperPuperPrinter», а проекту требовалось что-то более надежное и стабильное. Результат решения, которое предлагает Java, оказался неправильным…
Воспользуемся библиотекой javax.print:

import javax.print.PrintService;
import javax.print.PrintServiceLookup;
import javax.print.attribute.standard.PrinterMakeAndModel;
 
public class Tester {
    public static void main(String[] args) {
        final PrintService[] printServices = PrintServiceLookup.lookupPrintServices(null, null);
        for (PrintService printService : printServices) {
            System.out.println("PrinterName = " + printService.getName() +
                    ", PrinterMakeAndModel = " + printService.getAttribute(PrinterMakeAndModel.class));
        }
    }
}

Моя система показывает следующий результат:
PrinterName = Samsung SCX-4×28 Series PCL6, PrinterMakeAndModel = null
PrinterName = Samsung CLX-3180 Series, PrinterMakeAndModel = null
PrinterName = PDF Creator, PrinterMakeAndModel = null
PrinterName = FinePrint, PrinterMakeAndModel = null
PrinterName = Fax, PrinterMakeAndModel = null

Где информация о драйвере или модели принтера? Ее нет.
Посмотрим, что говорит об этом Oracle:

For attributes like
javax.print.attribute.standard.PrinterMakeAndModel
javax.print.attribute.standard.PrinterLocation
javax.print.attribute.standard.PrinterInfo
javax.print.PrintService.getAttribute should return meaningful information obtained from the Windows OS, instead of empty strings.
This bug can be reproduced always.
EVALUATION 2003-02-18
In Windows, this information can be retrieved from PRINTER_INFO_X structure.

ОК, проверим последнюю версию Java 1.7.0_17 – результат тот же, т.е. проблема 2003 не исправлена.

Ну да ладно, в принципе Java не предназначена знать все тонкости операционной системы, на которой она работает. Поэтому, как там Microsoft Windows разбирается со своими принтерами, должен был бы знать какой-нибудь Microsoft Windows сервис. Он есть – это библиотека c:\Windows\System32\Winspool.drv и ее функция EnumPrinters. Функция перечисляет принтеры системы и складывает информацию как раз в структуру PRINTER_INFO_2.

Чтобы получить доступ к библиотеке Winspool.drv можно использовать JNI – для этого нам придется писать оболочку на С, h-файл со стороны JNI и т.д. Для неспециалиста в этой технологии достаточно трудоемкий и время затратный процесс, а уж о трудностях отладки я вообще молчу.

Посмотрим, что нам может предложить JNA.

Начнем с самого оригинального примера «Hello World», что мне приходилось видеть, от разработчика JNA.

import com.sun.jna.Library;
import com.sun.jna.Native;
public class Tester {
    public interface Kernel32 extends Library {
        boolean Beep(int frequency, int duration);
    }
    private static Kernel32 kernel32 = (Kernel32) Native.loadLibrary("kernel32", Kernel32.class);
    private static void toMorseCode(String letter) throws Exception {
        for (byte b : letter.getBytes()) {
            kernel32.Beep(1200, ((b == '.') ? 50 : 150));
            Thread.sleep(50);
        }
    }
    public static void main(String[] args) throws Exception {
        String helloWorld[][] = {
                {"....", ".", ".-..", ".-..", "---"}, // HELLO
                {".--", "---", ".-.", ".-..", "-.."}  // WORLD
        };
        for (String word[] : helloWorld) {
            for (String letter : word) {
                toMorseCode(letter);
                Thread.sleep(150);
            }
            Thread.sleep(350);
        }
    }
}

Т.е. достаточно объявить интерфейс, расширяющий com.sun.jna.Library, объявить в нем необходимый метод с именем и сигнатурой, соответствующими функции из библиотеки (в данном случае это библиотека c:\Windows\System32\kernel32.dll и функция Beep), и загрузить библиотеку, используя этот интерфейс с помощью com.sun.jna.Native. Первым параметром выступает имя библиотеки, которое может содержать полный путь к ней в файловой системе, а вторым – класс интерфейса, который используется для связывания интерфейса и библиотеки посредством reflection.

Впечатляет, насколько просто можно услышать приветствие компьютера «Hello World», которое он пробибикает азбукой Морзе.

Также интересные примеры можно найти тут.

Теперь вернемся к задаче проекта – получить имена принтеров и их драйверов в Microsoft Windows. Как было сказано выше, нужно обратиться к Winspool.drv и посредством ее функции EnumPrinters получить набор структуры PRINTER_INFO_2 для каждого принтера в системе. К счастью JNA позаботился об обертке структур «нативных» библиотек и предоставила класс com.sun.jna.Structure.

Тестовый код выглядит так:

import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.Win32Exception;
import com.sun.jna.platform.win32.WinDef;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.win32.StdCallLibrary;
import com.sun.jna.win32.W32APIOptions;
import java.util.Arrays;
import java.util.List;
 
public class WinSpoolTest {
     
    public interface Winspool extends StdCallLibrary {
         
        boolean EnumPrinters(int flags, String name, int level, Pointer pPrinterEnum, int cbBuf, IntByReference pcbNeeded, IntByReference pcReturned);
        public static final int PRINTER_ENUM_LOCAL = 0x00000002;
        public static final int LEVEL2 = 2;
         
        public static class PRINTER_INFO_2 extends Structure {
            public String pServerName;
            public String pPrinterName;
            public String pShareName;
            public String pPortName;
            public String pDriverName;
            public String pComment;
            public String pLocation;
            public WinDef.INT_PTR pDevMode;
            public String pSepFile;
            public String pPrintProcessor;
            public String pDatatype;
            public String pParameters;
            public WinDef.INT_PTR pSecurityDescriptor;
            public int Attributes;
            public int Priority;
            public int DefaultPriority;
            public int StartTime;
            public int UntilTime;
            public int Status;
            public int cJobs;
            public int AveragePPM;
             
            @Override
            protected List<String> getFieldOrder() {
                return Arrays.asList(
                        "pServerName",
                        "pPrinterName",
                        "pShareName",
                        "pPortName",
                        "pDriverName",
                        "pComment",
                        "pLocation",
                        "pDevMode",
                        "pSepFile",
                        "pPrintProcessor",
                        "pDatatype",
                        "pParameters",
                        "pSecurityDescriptor",
                        "Attributes",
                        "Priority",
                        "DefaultPriority",
                        "StartTime",
                        "UntilTime",
                        "Status",
                        "cJobs",
                        "AveragePPM");
            }
            public PRINTER_INFO_2() {
            }
             
            public PRINTER_INFO_2(int size) {
                super(new Memory(size));
            }
        }
    }
     
    final static Winspool winspool = (Winspool) Native.loadLibrary("Winspool.drv", Winspool.class, W32APIOptions.UNICODE_OPTIONS);
     
    public static void main(String[] args) throws Exception {
        final IntByReference pcbNeeded = new IntByReference();
        final IntByReference pcReturned = new IntByReference();
        winspool.EnumPrinters(Winspool.PRINTER_ENUM_LOCAL, null, Winspool.LEVEL2, null, 0, pcbNeeded, pcReturned);
        if (pcbNeeded.getValue() <= 0) {
            return;
        }
        final Winspool.PRINTER_INFO_2 pPrinterEnum = new Winspool.PRINTER_INFO_2(pcbNeeded.getValue());
        if (!winspool.EnumPrinters(Winspool.PRINTER_ENUM_LOCAL, null, Winspool.LEVEL2, pPrinterEnum.getPointer(),
                pcbNeeded.getValue(), pcbNeeded, pcReturned)) {
            throw new Win32Exception(Kernel32.INSTANCE.GetLastError());
        }
        pPrinterEnum.read();
        final Winspool.PRINTER_INFO_2[] result = (Winspool.PRINTER_INFO_2[]) pPrinterEnum.toArray(pcReturned.getValue());
        for (Winspool.PRINTER_INFO_2 pi : result) {
            System.out.printf("PrinterName = %1$2s, DriverName = %2$2s\n", pi.pPrinterName, pi.pDriverName);
        }
    }
}

Что мы видим тут? Интерфейс Winspool теперь расширяет StdCallLibrary, а также содержит структуру PRINTER_INFO_2, наследницу Structure. Это необходимость, т.к. вызов Native.loadLibrary посредством reflection строит карту интерфейса и его структур и, если вынести эту структуру из интерфейса, то карта будет неправильной. Ладно, если бы была ошибка, так структура просто будет ошибочно заполнена методом EnumPrinters – например, вместо имени принтера в поле pPrinterName будет только первая буква имени.

У структуры PRINTER_INFO_2 есть еще пара особенностей: она должна содержать конструктор без параметров и переопределять метод protected List getFieldOrder() – строгая последовательность и именование полей.

Вот и все. Запустив тест, получим в консоли список принтеров и их драйверов:

PrinterName = Samsung SCX-4×28 Series PCL6, DriverName = Samsung SCX-4×28 Series PCL6
PrinterName = Samsung CLX-3180 Series, DriverName = Samsung CLX-3180 Series
PrinterName = PDF Creator, DriverName = CUSTPDF Writer
PrinterName = FinePrint, DriverName = FinePrint 7
PrinterName = Fax, DriverName = Microsoft Shared Fax Driver

Это именно то, что требовалось.
Как же можно еще упростить эту задачу? Выглядит все это все равно чересчур громоздко.
Например, нам не нужны все 21 поля структуры PRINTER_INFO_2, а только два – PrinterName и DriverName.
Посмотрим, как справляется с этим .NET – ведь это неотъемлемая часть Microsoft Windows.
Код для консольного приложения .NET будет выглядеть так:

public class Tester
{
    public static void Main()
    {
        System.Printing.LocalPrintServer localPrintServer = new
        System.Printing.LocalPrintServer(System.Printing.PrintSystemDesiredAccess.AdministrateServer);
        foreach (var printer in localPrintServer.GetPrintQueues())
        {
            System.Console.WriteLine("PrinterName = {0}, DriverName = {1}", printer.FullName, printer.QueueDriver.Name);
        }
        System.Console.Read();
    }
}

Результат выводится на консоль:

PrinterName = Samsung SCX-4×28 Series PCL6, DriverName = Samsung SCX-4×28 Series PCL6
PrinterName = Samsung CLX-3180 Series, DriverName = Samsung CLX-3180 Series
PrinterName = PDF Creator, DriverName = CUSTPDF Writer
PrinterName = FinePrint, DriverName = FinePrint 7
PrinterName = Fax, DriverName = Microsoft Shared Fax Driver

Хм… То же самое. Значит мы на правильном пути.

Как теперь обернуть этот код в «нативную» библиотеку, получить к ней доступ через JNA и забрать результат в удобном для нашего проекта виде?
Начнем с того, что .NET не «нативная» среда — у нее свои CLR (Common Language Runtime), CIL (Common Intermediate Language) и CLI (Common Language Infrastructure-Standard), и .NET-DLL будет отлично линковаться другой библиотекой или программой .NET, но никак не как «нативная» библиотека к С- и Java-программам.

Как же быть?

На помощь приходит Unmanaged Exports (DllExport for .Net).

Ее можно подключить как nuget-плагин через Package Manager Console с помощью команды:

PM> Install-Package UnmanagedExports

Либо просто взять у производителя архив с плагином UnmanagedExportLibrary.zip и скопировать его в каталог шаблонов проектирования Microsoft Visual Studio.

Обычно это My Documents\Visual Studio 20**\Templates\ProjectTemplates.

Потом создать новый проект C# на базе шаблона UnmanagedExportLibrary и наша «нативная» библиотека Winspool.dll почти готова.
Добавим в нее статический класс Export и статическую функцию GetPrinterInfo:

using System;
using System.Text;
using RGiesecke.DllExport;
 
public static class Export
{
    [DllExport("GetPrinterInfo")]
    public static string GetPrinterInfo()
    {
        try
        {
            System.Text.StringBuilder sb = new System.Text.StringBuilder();
            System.Printing.LocalPrintServer localPrintServer = new System.Printing.LocalPrintServer(System.Printing.PrintSystemDesiredAccess.AdministrateServer);
            foreach (var printer in localPrintServer.GetPrintQueues())
            {
                sb.Append(printer.FullName).Append(";").Append(printer.QueueDriver.Name).Append(";");
            }
            return sb.ToString();
        }
        catch (Exception ex)
        {
            return ex.Message;
        }
    }
}

Статический класс и статическая функция – это необходимое условие для применения атрибута DllExport из библиотеки RGiesecke.DllExport. Таким образом, функция станет «нативной» в библиотеке и к ней можно будет непосредственно обращаться через JNA.

Функция GetPrinterInfo собирает информацию о принтерах и их драйверах в строку с разделителем «;», т.е. если потом преобразовать эту строку по разделителю в массив строк, то по нечетным индексам будет имя принтера, а по четным – имя драйвера. Очень полезным для нас тут будет то, что DllExport может передавать простые типы в качестве параметров и возвращаемых значений функций, и к счастью для нас, что в этот список входит string.

Скомпилируем библиотеку Winspool.dll для платформы х86.

Код на Java выглядит так:

import com.sun.jna.Native;
import java.io.File;
 
public class WinSpoolTest {
 
    public interface Winspool extends com.sun.jna.Library {
        public String GetPrinterInfo();
    };
 
    public static void main(String[] args){
        final String path = new File("").getAbsolutePath() + "\\lib\\Winspool.dll";
        final Winspool lib = (Winspool) Native.loadLibrary(path, Winspool.class);
 
        final String res = lib.GetPrinterInfo();
        final String[] arr = res.split(";");
 
        for (int i = 0; i < arr.length; i += 2){
            System.out.printf("PrinterName = %1$2s, DriverName = %2$2s\n", arr[i], arr[i+1]);
        }
    }
}

Выглядит значительно проще, не правда ли?

Но что-то все равно бросается в глаза своим несовершенством… Да, это расположение библиотеки Winspool.dll в папке lib, рядом с классом, и ее поиск по пути в файловой системе:

final String path = new File(«»).getAbsolutePath() + «\\lib\\Winspool.dll»;

В данном случае это не критично, но что делать, если проект как-то хитро компилируется, использует специальные папки для ресурсов, или динамически ориентирован на Java определенной разрядности – 64 бита или 32? Как сделать так, чтобы нам упростить поиск месторасположения библиотеки? И как подготовить «нативную» библиотеку для разной разрядности, да еще отследить в Java проекте, когда и какую использовать?

Вот тут выступает еще одно достоинство JNA – автоматическое сканирование и нахождение «нативных» библиотек.

Если сделать так:
1 – где-нибудь создать две папки — win32-x86 и win32-x86-64
2 — скомпилировать «нативную» библиотеку с атрибутом х86, и положить ее в папку win32-x86
3 — скомпилировать с атрибутом х64, и положить в win32-x86-64
4 — создать из этих двух папок zip-архив
5 — переименовать его в jar
6 — подключить его к Java-проекту
то JNA автоматически подключает версию «нативной» библиотеки соответственно разрядности Java.

Код станет еще проще, и нам не придется устраивать поиск библиотеки Winspool.dll, контроль соответствия разрядности Java и библиотеки Winspool.dll.

import com.sun.jna.Native;
 
public class WinSpoolTest2 {
 
    public interface Winspool extends com.sun.jna.Library {
        public String GetPrinterInfo();
    };
 
    public static void main(String[] args){
        final Winspool lib = (Winspool) Native.loadLibrary("Winspool", Winspool.class);
 
        final String res = lib.GetPrinterInfo();
        final String[] arr = res.split(";");
 
        for (int i = 0; i < arr.length; i += 2){
            System.out.printf("PrinterName = %1$2s, DriverName = %2$2s\n", arr[i], arr[i+1]);
        }
    }
}

Ну и напоследок, можно сделать еще красивее. Благодаря тому, что «нативная» библиотека .NET может возвращать строку неограниченной длинны, то использование результата с разделителем выглядит не очень дружественно.

Что более всего подходит для передачи данных в виде строки? Правильно – XML. Попробуем передать в качестве результата «нативной» библиотеки XML-сериализованный объект, а на стороне Java десериализуем в свой точно такой же объект.

На C# есть несколько возможностей XML-сериализации объектов, но сразу сделаю замечание, что распространенная сериализация из библиотеки System.Xml.Serialization не работает, если использовать ее в «нативной» библиотеке.

Будет ошибка System.IO «Illegal characters in path», хотя никакого IO при этом не используется – это какой-то баг .NET, который пока не исправлен и в .NET 4.5.

К счастью есть другой механизм XML-сериализации в библиотеке System.Runtime.Serialization.

Добавим к .NET-проекту класс PrintInfoList:

using System;
using System.Runtime.Serialization;
using System.Text;
using System.IO;
using System.Collections.Generic;
 
[DataContract(Name = "PrintInfoList")]
public class PrintInfoList
{
    [DataContract(Name = "PrintInfo")]
    public class PrintInfo
    {
        [DataMember(Name = "PrinterName")]
        public string PrinterName
        { get; set; }
 
        [DataMember(Name = "DriverName")]
        public string DriverName
        { get; set; }
    }
 
    private List<PrintInfo> list = new List<PrintInfo>();
 
    [DataMember(Name = "List")]
    public List<PrintInfo> List
    { get { return list; } }
 
    public String Serialize()
    {
        DataContractSerializer serializer = new DataContractSerializer(typeof(PrintInfoList));
        using (MemoryStream memoryStream = new MemoryStream())
        {
            serializer.WriteObject(memoryStream, this);
            memoryStream.Flush();
            string result = Encoding.UTF8.GetString(memoryStream.ToArray());
            result = System.Text.RegularExpressions.Regex.Replace(result, "<PrintInfoList.*?>", "<PrintInfoList>");
            return result;
        }
    }
}

Здесь можно видеть корневой класс PrintInfoList, который является списком объектов PrintInfo. Стоит обратить внимание на использование атрибутов библиотеки System.Runtime.SerializationDataContract и DataMember. Их применение должно быть совершенно очевидным – DataContract описывает объект, а DataMember – данные объекта, в данном случае это открытые поля. Кроме того, в конце метода десериализации в полученной XML-строке применена замена секции <PrintInfoList… со всеми пространствами имен Namespaces на — просто секцию <PrintInfoList>. Это нужно для упрощения примера, иначе придется обеспечивать обработку всех Namespaces на стороне Java.

Далее в класс Export добавим новую функцию GetPrinterInfo2, возвращающую заполненный и сериализованный объект PrintInfoList в виде XML-строки:

[DllExport("GetPrinterInfo2")]
public static string GetPrinterInfo2()
{
    try
    {
        PrintInfoList pil = new PrintInfoList();
        System.Printing.LocalPrintServer localPrintServer = new System.Printing.LocalPrintServer(System.Printing.PrintSystemDesiredAccess.AdministrateServer);
        foreach (var printer in localPrintServer.GetPrintQueues())
        {
            pil.List.Add(new PrintInfoList.PrintInfo
            {
                PrinterName = printer.FullName,
                DriverName = printer.QueueDriver.Name
            });
        }
        return pil.Serialize();
    }
    catch (Exception ex)
    {
        return ex.Message;
    }
}

Теперь вернемся к Java. Чтобы десериализовать XML-строку — результат метода GetPrinterInfo2 библиотеки Winspool.dll, создадим полный аналог объектов на Java:

import com.sun.jna.Native;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.*;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.List;
 
public class WinSpoolTest3 {
 
    @XmlAccessorType(XmlAccessType.NONE)
    @XmlRootElement(name = "PrintInfo")
    public static class PrintInfo {
         
        @XmlElement(name = "PrinterName")
        public String printerName;
 
        @XmlElement(name = "DriverName")
        public String driverName;
    }
 
    @XmlAccessorType(XmlAccessType.NONE)
    @XmlRootElement(name = "PrintInfoList")
    public static class PrintInfoList {
 
        @XmlElementWrapper(name = "List")
        @XmlElements({@XmlElement(name = "PrintInfo", type = PrintInfo.class)})
        public List<PrintInfo> list;
 
        public static PrintInfoList deserialize(String xml) {
            try {
                final InputStream inputStream = new ByteArrayInputStream(xml.getBytes("UTF-8"));
                try {
                    final JAXBContext context = JAXBContext.newInstance(PrintInfoList.class);
                    final Unmarshaller unmarshaller = context.createUnmarshaller();
                    return (PrintInfoList)unmarshaller.unmarshal(inputStream);
                } finally {
                    inputStream.close();
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
            return null;
        }
    }
 
    public interface Winspool extends com.sun.jna.Library {
        public String GetPrinterInfo2();
    };
 
    public static void main(String[] args){
        final Winspool lib = (Winspool) Native.loadLibrary("Winspool", Winspool.class);
        final String res = lib.GetPrinterInfo2();
 
        final PrintInfoList pil = PrintInfoList.deserialize(res);
 
        for(final PrintInfo pi : pil.list){
            System.out.printf("PrinterName = %1$2s, DriverName = %2$2s\n", pi.printerName, pi.driverName);
        }
    }
}

Т.е. XML-строка десериализуется в экземпляр PrintInfoList, который представляет собой список объектов PrintInfo, и который теперь гораздо удобнее и нагляднее использовать в любых частях проекта.

На базе полученного шаблона «нативная» DLL -> JNA -> Java можно создавать много разных и интересных межплатформенных проектов. На C# можно писать библиотеки-оболочки, упрощающие работу с «внутренностями» Microsoft Windows, которые трудно реализуемы или вообще недоступны для Java, а качестве обмена между .NET и Java процессами использовать XML-сериализованные объекты. Ведь этот принцип можно использовать и в обратном направлении, т.е. от Java к .NET, и таким способом передавая параметры и прочие данные для обработки в «нативной» DLL через XML-сериализованные объекты.

Исходные коды и прочие ресурсы:

Все исходные коды можно найти здесь:
src-jna-for-access-to-native-com-dll-library.zip

Опубликовано в актуально, новости, статья