Как написать программу для принтера

Обновлено: 03.07.2024

Драйвер-это основа взаимодействия системы с устройством в ОС Windows.Это одновременно удобно и неудобно.
Про удобства я разъяснять не буду - это и так понятно,
а заострюсь я именно на неудобствах драйверов.
В сложившейся ситуации пользователь полностью подчинён воле производителя
- выпусти тот драйвер - хорошо, а не выпустит.
Только продвинутый пользователь, имеющий голову на плечах
(особенно, если он ешё и программер) не станет мириться с таким положением дел
- он просто возьмёт и сам напишет нужный драйвер.
Это нужно и взломщику: драйвер - это удобное окошко в ring0,
которое является раем для хакера. Но хоть написать драйвер и просто,
да не совсем - есть масса подводных камней. Да и документированность данного вопроса на русском языке оставляет желать лучшего.
Этот цикл статей поможет тебе во всём разобраться.
Приступим.

Хочу сразу же сделать несколько предупреждений.
Данная статья всё-таки подразумевает определённый уровень подготовки.
Драйвера-то ведь пишутся на C(++) с большим количеством ассемблерных вставок.
Поэтому хорошее знание обоих языков весьма желательно (если не сказать - обязательно).
Если же ты пока не можешь этим похвастаться,
но желание писать драйвера есть - что ж, так как эта статья вводная, в конце её будет приведён список полезной литературы,
ссылок и т.д. Но помни: учить тебя в этом цикле статей программированию как таковому я тебя не буду.
Может как-нибудь в другой раз. Согласен? Тогда поехали!


Скоро здесь, возможно, будет стоять твоё имя.

Практически в любом деле, как мне кажется, нужно начинать с теории.
Вот и начнём с неё. Для начала уясним себе поточнее основные понятия.
Первое: что есть драйвер? Драйвер - в сущности
кусок кода ОС, отвечающий за взаимодействие с аппаратурой.
Слово "аппаратура" в данном контексте следует понимать в самом широком смысле.
С момента своего появления как такого до сегодняшнего дня драйвер беспрерывно эволюционировал.
Вот, скажем, один из моментов его развития. Как отдельный и довольно независимый модуль драйвер сформировался не сразу.
Да и сейчас этот процесс до конца не завершён:
ты наверняка сталкивался с тем, что во многих
дистрибутивах никсов для установки/перестановки etc драйверов нужно перекомпилировать ядро,
т.е. фактически заново пересобирать систему.
Вот, кстати ещё один близкий моментец: разные принципы работы с драйверами в Windows 9x и NT.
В первом процесс установки/переустановки драйверов проходит практически без проблем,
во втором же случае это тяжёлое и неблагодарное дело,
для "благополучного" завершения которого нередко приходится прибегать к полной переустановке ОС.
А зато в Windows 9x. так,стоп,открывается широкая и волнующая тема,
которая уведёт меня далеко от темы нынешней статьи,
так что вернёмся к нашим баранам. ой,то есть к драйверам.
В порядке общего развития интересно сравнить особенности драйверов в Windows и *nix(xBSD) системах:

1) Способ работы с драйверами как файлами (подробнее см. ниже)
2) Драйвер, как легко заменяемая честь ОС (учитывая уже сказанные выше примечания)
3) Существование режима ядра

Теперь касательно первого пункта. Это значит,
что функции, используемые при взаимодействии с файлами,
как и с драйверами, практически идентичные (имеется в виду лексически):
open, close, read и т.д. И напоследок стоит отметить идентичность механизма
IOCTL (Input/Output Control Code-код управления вводом-выводом)
-запросов.

Драйвера под Windows делятся на два типа:
Legacy (устаревший) и WDM (PnP). Legacy драйверы (иначе называемые "драйверы в стиле
NT") чрезвычайно криво работают (если работают вообще)
под Windows 98, не работают с PnP устройствами, но зато могут пользоваться старыми функциями
HalGetBusData, HalGetInterruptVector etc, но при этом не имеют поддержки в лице шинных драйверов.
Как видишь, весьма средненький драйвер. То ли дело
WDM: главный плюс - поддержка PnP и приличненькая совместимость:
Windows 98, Me, 2000, XP, 2003, Server 2003 и т.д. с вариациями; но он тоже вынужден за это расплачиваться:
например, он не поддерживает некоторые устаревшие функции
(которые всё таки могут быть полезны). В любом случае,
не нужно ничего воспринимать как аксиому, везде бывают свои исключения.
В некоторых случаях лучше написания Legacy драйвера ничего не придумать.

Как ты наверняка знаешь, в Windows есть два мода работы:
User Mode и Kernel Mode - пользовательский режим и режим ядра соответственно.
Первый - непривилегированный, а второй - наоборот.
Вот во втором чаще всего и сидят драйвера (тем
более, что мы в данный момент говорим именно о драйверах режима ядра).
Главные различия между ними: это доступность всяких привилегированных команд процессора.
Программировать (а уж тем более качественно) в Kernel mode посложнее будет,
чем писать прикладные незамысловатые проги.
А драйвера писать без хорошего знания Kernel mode - никак.
Нужно попариться над назначением выполнения разнообразных работ отдельному подходящему уровню IRQL, желательно выучить новое API (так как в Kernel mode API отличается от прикладного).
в общем, предстоит много всяких радостей. Но тем не менее,
это очень интересно, познавательно, и даёт тебе совершенно иной уровень власти над компьютером.

А раз уж я упомянула про IRQL, разьясню и это понятие.
IRQL (Interrupt Request Level - уровень приоритета выполнения) - это приоритеты,
назначаемые специально для кода, работающего в режиме ядра.
Самый низкий уровень выполнения - PASSIVE_LEVEl. Работающий поток может быть прерван потоком только с более высоким
IRQL.

Ну и напоследок разъясним ещё несколько терминов:

1) ISR (Interrupt Service Routine) - процедура обслуживания прерываний.
Эта функция вызывается драйвером в тот момент,
когда обслуживаемая им аппаратура посылает сигнал прерывания.
Делает самые необходимые на первый момент вещи:
регистрирует callback - функцию и т.д.

2) DpcForISR (Deferred Procedure Call for ISR) - процедура отложенного вызова для обслуживания прерываний.
Эту функцию драйвер регистрирует в момент работы ISR для выполнения основной работы.

3) IRP (Input/Output Request Packet) - пакет запроса на ввод - вывод.
Пакет IRP состоит из фиксированной и изменяющейся частей.
Вторая носит название стека IRP или стека ввода - вывода (IO stack).

4) IO stack location - стек ввода - вывода в пакете IRP.

5) Dispatch Routines (Рабочие процедуры) - эти функции регистрируются в самой первой (по вызову) процедуре драйвера.

6) Major IRP Code - старший код IRP пакета.

7) Minor IRP Code - соответственно, младший код IRP пакета.

8) DriverEntry - эта функция драйвера будет вызвана первой при его загрузке.

9) Layering (Многослойность) - данной возможностью обладают только WDM - драйвера.
Она заключается в наличии реализации стекового соединения между драйверами.
Что такое стековое соединение? Для этого необходимо знать про Device
Stack (стек драйверов) - поэтому я обязательно вспомню про всё это чуточку ниже.

10) Device Stack, Driver Stack (стек устройств, стек драйверов) - всего лишь
объемное дерево устройств. Его, кстати, можно рассмотреть во всех подробностях с помощью программы
DeviceTree (из MS DDK), например.

11) Стековое соединение - как и обещала, объясняю. В стеке драйверов самый верхний драйвер - подключившийся позднее.
Он имеет возможность посылать/переадресовывать IRP запросы другим драйверам,
которые находятся ниже его. Воти всё. Правда,просто?

12) AddDevice - функция, которую обязательно должны поддерживать WDM драйверы.
Её название говорит само за себя.

13) Device Object, PDO, FDO (Объект устройства, физический,
функциональный) - при подключении устройства к шине она создаёт PDO.
А уже к PDO будут подключаться FDO объекты WDM драйверов.
Обьект FDO создаётся самим драйвером устройства при помощи функции IOCreateDevice.
Обьект FDO также может иметь свою символическую ссылку, от которой он будет получать запросы от драйвера.
Это что касается WDM драйверов. С драйверами "в стиле NT" ситуация несколько иная.
Если он не обслуживает реальных/PnP устройств,
то PDO не создаётся. Но для связи с внешним миром без FDO не обойтись.
Поэтому он присутствует и тут.

14) Device Extension (Расширение обьекта устройства) - "авторская" структура,
т.е. она полностью определяется разработчиком драйвера.
Правилом хорошего тона считается, например,
размещать в ней глобальные переменные.

15) Monolithic Driver (Монолитный драйвер) - это драйвер,
который самостоятельно обрабатывает все поступающие
IRP пакеты и сам работает с обслуживаемым им устройством
(в стеке драйверов он не состоит). Данный тип драйверов используется только если обслуживается не
PnР устройство или же всего лишь требуется окошко в ring0.

16) DIRQL (уровни аппаратных прерываний) -
прерывания, поступающие от реальных устройств, имеют наивысший приоритет IRQL,
поэтому для них решено было придумать специальное название
(Device IRQL).

17) Mini Driver (Мини - драйвер) - чуть меньше "полного" драйвера.
Обычно реализуется в виде DLL-ки и имеет оболочку в виде "полного" драйвера.

18) Class Driver (Классовый драйвер) - высокоуровневый драйвер,
который предоставляет поддержку класса устройств.

19) РnP Manager (PnP менеджер) - один из главных компонентов операционной системы.
Состоит из двух частей: PnP менеджера пользовательского и "ядерного" режимов.
Первый в основном взаимодействует с пользователем;
когда тому нужно, например, установить новые драйвера и т.д.
А второй управляет работой, загрузкой и т.д. драйверов.

20) Filter Driver (фильтр - драйвер) - драйверы, подключающиеся к основному драйверу либо сверху
(Upper), либо снизу (Lower). Фильтр драйверы (их может быть несколько) выполняют фильтрацию IRP пакетов.
Как правило, для основного драйвера Filter Drivers неощутимы.

21) Filter Device Object - объект устройства, создаваемый фильтр - драйвером.

22) HAL (Hardware Abstraction Layer) - слой аппаратных абстракций.
Данный слой позволяет абстрагироваться компонентам операционной системы от особенностей конкретной платформы.

23) Synchronization Objects (Обьекты синхронизации) - с помощью этих
объектов потоки корректируют и синхронизируют свою работу.

24) Device ID - идентификатор устройства.

25) DMA (Direct Memory Access) - метод обмена данными между устройством и памятью
(оперативной) в котором центральный процессор не принимает участия.

25) Polling - это особый метод программирования, при котором не устройство посылает сигналы прерывания драйверу,
а сам драйвер периодически опрашивает обслуживаемое им устройство.

26) Port Driver (Порт-драйвер) - низкоуровневый драйвер,
принимающий системные запросы. Изолирует классовые драйверы устройств от аппаратной специфики последних.

Ну вот, пожалуй, и хватит терминов. В будущем,
если нужны будут какие-нибудь уточнения по теме,
я обязательно их укажу. А теперь, раз уж эта статья
теоретическая, давай-ка взглянем на архитектуру Windows NT с высоты птичьего полёта.

Краткий экскурс в архитектуру Windows NT

Наш обзор архитектуры Windows NT мы начнём с разговора об уровнях разграничения привилегий. Я уже упоминала об user и kernel mode.
Эти два понятия тесно связаны с так называемыми кольцами (не толкиеновскими ).
Их ( колец) в виде всего четыре: Ring3,2,1 и 0. Ring3 - наименее привилегированное кольцо,
в котором есть множество ограничений по работе с устройствами,
памятью и т.д. Например, в третьем кольце нельзя видеть адресное пространство других приложений без особого на то разрешения. Естественно,
трояну вирусу etc эти разрешения получить будет трудновато, так что хакеру в третьем кольце жизни никакой. В третьем кольце находится user mode. Kernel mode сидит в нулевом кольце - наивысшем уровне привилегий. В этом кольце можно всё:
смотреть адресные пространства чужих приложений без каких - либо ограничений и разрешений, по своему усмотрению поступать с любыми сетевыми пакетами, проходящими через машину, на всю жизнь скрыть какой-нибудь свой процесс или файл и т.д. и т.п. Естественно,
просто так пролезть в нулевое кольцо не получиться:
для этого тоже нужны дополнительные телодвижения. У легального драйвера с этим проблем нет:
ему дадут все необходимые API - шки, доступ ко всем нужным системным таблицам и проч. Хакерской же нечисти опять приходиться туго:
все необходимые привилегии ему приходиться "выбивать"
незаконным путём. Но это уже тема отдельной статьи, и мы к ней как-нибудь ещё вернёмся. А пока продолжим.

У тебя наверняка возник законный вопрос:
а что же сидит в первом и втором кольцах ? В том то всё и дело,
что программисты из Microsoft почему - то обошли эти уровни своим вниманием. Пользовательское ПО сидит в user mode,а всё остальное (ядро,
драйвера. ) - в kernel mode. Почему они так сделали - загадка, но нам это только на руку. А теперь разберёмся с компонентами (или, иначе говоря, слоями ) операционной системы Windows
NT.

Посмотри на схему - по ней многое можно себе уяснить. Разберём её подробнее.
С пользовательским режимом всё понятно. В kernel mode самый низкий уровень аппаратный. Дальше идёт HAL, выше - диспетчер ввода - вывода и драйвера устройств в одной связке, а также ядрышко вместе с исполнительными компонентами. О HAL я уже говорила, поэтому поподробнее поговорим об исполнительных компонентах. Что они дают? Прежде всего они приносят пользу ядру. Как ты уже наверняка уяснил себе по схеме, ядро отделено от исполнительных компонентов. Возникает вопрос:
почему ? Просто на ядре оставили только одну задачу:
просто управление потоками, а все остальные задачи (управление доступом,
памятью для процессов и т.д.) берут на себя исполнительные компоненты (еxecutive). Они реализованы по модульной схеме, но несколько компонентов её (схему) не поддерживают . Такая концепция имеет свои преимущества:
таким образом облегчается расширяемость системы. Перечислю наиболее важные исполнительные компоненты:

1) System Service Interface (Интерфейс системных служб )
2) Configuration Manager (Менеджер конфигурирования)
3) I/O Manager (Диспетчер ввода-вывода,ДВВ)
4) Virtual Memory Manager,VMM (Менеджер виртуальной памяти)
5) Local Procedure Call,LPC (Локальный процедурный вызов )
6) Process Manager (Диспетчер процессов)
7) Object Manager (Менеджер объектов)

Так как эта статья - первая в цикле, обзорная, подробнее на этом пока останавливаться не будем. В процессе практического обучения написанию драйверов, я буду разъяснять все неясные термины и понятия. А пока перейдём к API.

API (Application Programming Interface) - это интерфейс прикладного программирования. Он позволяет обращаться прикладным программам к системным сервисам через их специальные абстракции. API-интерфейсов несколько, таким образом в Windows-системах присутствуют несколько подсистем. Перечислю:

1) Подсистема Win32.
2) Подсистема VDM (Virtual DOS Machine - виртуальная ДОС - машина)
3) Подсистема POSIX (обеспечивает совместимость UNIX - программ)
4) Подсистемиа WOW (Windows on Windows). WOW 16 обеспечивает совместимость 32-х разрядной системы с 16-битными приложениями. В 64-х разрядных системах есть подсистема WOW 32,
которая обеспечивает аналогичную поддержку 32 - битных приложений.
5) Подсистема OS/2. Обеспечивает совместимость с OS/2 приложениями.

Казалось бы, всё вышеперечисленное однозначно говорит в пользу WINDOWS NT систем!
Но не всё так хорошо. Основа WINDOWS NT (имеются ввиду 32-х разрядные версии) - подсистема Win32. Приложения, заточенные под одну подсистему не могут вызывать функции другой. Все остальные (не Win32) подсистемы существуют в винде только в эмуляции и реализуются функции этих подсистем только через соответствующие функции винды. Убогость и ограниченность приложений, разработанных, скажем, для подсистемы POSIX и запущенных под винду - очевидны.
Увы.

Подсистема Win32 отвечает за графический интерфейс пользователя, за обеспечение работоспособности Win32 API и за консольный ввод - вывод. Каждой реализуемой задаче
соответствуют и свои функции: функции, отвечающие за графический фейс,
за консольный ввод - вывод (GDI - функции) и функции управления потоками,
файлами и т.д. Типы драйверов, наличествующие в Windows, я уже упоминала в разделе терминов:
монолитный драйвер, фильтр - драйвер и т.д. А раз так, то пора закругляться. Наш краткий обзор архитектуры Windows NT можно считать завершённым. Этого тебе пока хватит для общего понимания концепций Windows NT, и концепций написания драйверов под эту ось - как следствие.

Инструменты

Описать и/или упомянуть обо всех утилитах, могущих понадобиться при разработке драйверов - немыслимо. Расскажу только об общих направлениях.

Без чего нельзя обойтись ни в коем случае - это Microsoft DDK (Driver Development Kit ). К этому грандиозному пакету прилагается и обширная документация. Её ценность - вопрос спорный. Но в любом случае, хотя бы ознакомиться с первоисточником информации по написанию драйверов для Windows - обязательно. В принципе, можно компилять драйвера и в Visual Studio, но это чревато долгим и нудным копанием в солюшенах и vcproj-ектах, дабы код твоего драйвера нормально откомпилировался. В любом случае, сорцы придётся набивать в визуальной студии, т.к. в DDK не входит
нормальная IDE. Есть пакеты разработки драйверов и от третьих фирм:
WinDriver или NuMega Driver Studio, например. Но у них есть отличия от майкрософтовского базиса функций (порой довольно большие ) и многие другие мелкие неудобства. Так что DDK - лучший вариант. Если же ты хочешь писать драйвера исключительно на ассемблере, тебе подойдёт KmdKit (KernelMode Driver DevelopmentKit) для MASM32. Правда, этот вариант только для Win2k/XP.

Напоследок нельзя не упомянуть такие хорошие проги, как PE
Explorer, PE Browse Professional Explorer, и такие незаменимые, как дизассемблер IDA и лучший отладчик всех времён и народов SoftICE.

Ну вот и подошла к концу первая статья из цикла про написание драйверов под Windows. Теперь ты достаточно "подкован" по
теоретической части, так что в следующей статье мы перейдём к практике. Желаю тебе удачи в этом интереснейшем деле - написании драйверов! Да не облысеют твои пятки!

Приветствую всех. В сегодняшней статье речь пойдёт о том, как можно реализовать собственный высокоуровневый API в управляемом коде для работы с устройствами печати, от установки нового монитора печати в системе и до получения обработанного драйвером устройства печати документа с порта принтера.

Постановка задачи

Сперва я предлагаю обрисовать примерный список задач, которые нам необходимо будет решать в данной статье. Пускай он будет выглядеть следующим образом:

  • Возможность управлять полным циклом установки устройства печати в системе;
  • Возможность управлять полным циклом удаления устройства печати из системы;
  • Возможность конфигурировать устройство печати;
  • По возможности, оптимизировать повторное использование кода в проекте, а так же сделать структуру API максимально функциональной и удобной;
  • Максимально возможно повысить отказоустойчивость API, но при этом сохранить доступ к низкоуровневым исключениям Win32;
  • Обеспечить совместимость с системами, начиная с Windows XP и заканчивая Windows 10;
  • Возможность обработки данных печати в коде, использующем наш API.

Немного теории

Я не буду рассматривать в данной статье теоретическую базу по печати документов, различиям между принтерами с поддержкой PCL/PostScript и принтерами GDI и прочую базу. Всю необходимую информацию по данной тематике можно в избытке найти на просторах сети. Но кое-что знать всё же необходимо, чтобы лучше понимать как реализовать поставленные перед нами задачи.

Начнём с самого главного — процессом печати в Windows рулит служба Spooler. В директории C:/Windows/System32/ имеется её собственный низкоуровневый драйвер winspool.drv, в котором заложено множество точек входа для обращения к службе печати и выполнения целого ряда действий, от получения системной директории драйверов печати и имени установленного по умолчанию принтера в системе до манипуляций с очередью задач на печать. Для тех, кто хочет написать собственный монитор (о чём, возможно, я когда-нибудь так же расскажу в одной из будущих статей), помимо winspool.h ещё понадобится winsplp.h из DDK, представляющий дополнительный функционал для сборки драйвера в соответствии спецификации Spooler.

Далее, полнофункциональное устройство печати в Windows, говоря простым языком, состоит из монитора печати, открытого на мониторе порта, собственно устройства печати (принтера) и предварительно установленного драйвера к нему (рис. 1). На одном мониторе может быть открыто сразу несколько портов, к одному порту может быть привязано сразу несколько принтеров. Это важно учитывать при удалении того или иного компонента из системы, если, например, Вы захотите удалить определённый порт, предварительно нужно будет снести и все устройства печати, которые к нему привязаны.

Рисунок 1

P/Invoke

Пример 1. Вызов метода GetPrinterDriverDirectory из драйвера winspool.drv.
Первым делом, нам нужно знать, что возвращает метод и что принимает в аргументах при вызове. Для этого лезем в документацию и читаем описание сигнатуры метода. Обратите внимание, в дальнейшем к документации низкоуровневого API нам придётся обращаться сплошь и рядом, далее по ходу статьи я не буду больше указывать о необходимости этого действия при реализации тех или иных методов/структур, т.к. они требуются по умолчанию.


Атрибут DllImportAttribute позволяет нам указать параметры обращения к низкоуровневой точке входа. В WinAPI большинство функций написаны с учётом двух основных кодировок — Unicode и ANSI, функции имеют окончания W и A соответственно. Если нам нужно обратиться к конкретному методу в кодировке, но при этом мы не хотим рефакторить основное имя описываемого метода, мы можем указать имя точки входа явно, передав его в соответствующий аргумент атрибута (например, GetPrinterDriverDirectoryW). Так же при этом не забываем указать аргумент CharSet = CharSet.Unicode (в нашем случае, кодировка определяется автоматически). По всем другим полезным атрибутам можно найти информацию в официальной документации.

Пример 2. Вызов метода AddMonitor из драйвера winspool.drv.


Здесь важно понимать, что структуры, говоря «народным» языком, — это набор выделенных участков памяти определенного размера, хранящих значения. Каждый из таких участков представляет собой поле данных определённого типа. Ни о каких именах полей и их атрибутах речи не идёт, никаких метаданных, присущих изменяемым типам (классам), только размеры выделяемой памяти для хранения данных. Это означает, что имена членов структуры, как и само имя структуры, могут быть любыми, важно соблюсти соответствие выделяемых размеров памяти каждому члену. Для таких целей существует атрибут StructLayoutAttribute, позволяющий управлять физическим размещением полей данных класса или структуры в памяти. Есть множество способов управления размещением данных полей в памяти, можно явно задавать смещение полей, указывать абсолютный размер структуры, задавать кодировку для способа маршалирования, упаковку, размещать сегменты полей в порядке очерёдности друг за другом и т.д. Примеры реализаций этих способов можно найти здесь. Конкретно для нашей задачи отлично подойдёт последний способ, который мы указываем через LayoutKind.Sequential.


Как это работает: мы объявили структуру, указали в атрибуте размещение полей в памяти с помощью LayoutKind.Sequential, указали типы данных полей, для WinAPI типы данных в структурах — значимые, а значит их размер нам известен, в неуправляемом коде это sizeof(), в управляемом — Marshal.SizeOf(). Всё.

Маршалирование

Архитектура будущего API

С теорией разобрались, пора продумать архитектуру нашего будущего API. Здесь нет какой-либо конкретной или оптимальной философии, каждый сам выбирает нужные паттерны проектирования кода в зависимости от условий решаемой задачи. Для нашего случая я решил поступить следующим образом: весь код будущей библиотеки будет состоять из двух основных модулей — «фабрики» классов и интерфейсов, реализующих эти классы. Публичная реализация будет давать возможность получить список всех установленных компонентов в системе, установить/удалить компонент и прочее. Внутренняя реализация будет работать с маршалированием и P/Invoke. Для специфических случаев мы сможем создавать экземпляры наших классов и вызывать их методы, для базовых случаев будем обращаться к нашей «фабрике». Визуально это всё можно представить примерно следующим образом (рис. 2):


Для решения задачи в рамках статьи нам понадобятся реализации IMonitor, IPort, IDriver и IPrinter, собственно сам класс фабрики PrintingApi, а так же вспомагательные флаги. Остальное пока что опустим.

Кодовая база

Первым делом, давайте напишем базовый интерфейс для всех наших компонентов печати:


Здесь всё просто, у каждого компонента будет имя и два метода для установки/удаления в системе, с возможностью работы с удалённой машиной.

Теперь напишем основу для нашей фабрики:

Думаю, из документированных комментариев понятно что здесь к чему. Синглтоном создаём новый статический экземпляр класса в статическом конструкторе, описываем два делегата EnumInfo и EnumInfo2 для вызова нативных методов получения данных в наших будущих классах, описываем методы-хэлперы над нативными методами.

В большинстве случаев, весь процесс работы с нативными методами будет сводиться к следующей последовательности действий:

    Вызвать метод в первый раз, чтобы он вернул false и код нативной ошибки 122 («неинициализированный буфер»). В аргументах при этом мы указываем дефолтные значения аргументов (нулевые) для обработки буфера. Это нужно для того, чтобы метод вернул нам изменённые значения инициализации буфера и передал их в наши переменные по ссылке, после того как метод отработает, у нас будет необходимая информация для вызова метода с уже корректными значениями аргументов для инициализации буфера;

Для перехвата и генерации исключений в нашем API предусмотрим отдельный класс:

Здесь мы сразу прописали для удобства основные коды нативных ошибок при работе со службой печати.

Теперь нам понадобится реализовать пару перечислимых типов для более удобной работы с кодом и минимизации передачи неверных аргументов в нативные методы:


Для преобразования Environment в строку и наоборот, реализуем два метода расширения:


Вместо двух методов расширения и enum Environment можно обойтись обычными строковыми константами, я пошёл таким путём просто потому, что хотел изначально запретить передавать в имени окружения невалидную отсебятину, но сохранить при этом возможность выбирать из заранее заданного ограниченного набора строк.

Дабы не писать в каждом классе реализации перегрузок методов установки и удаления компонента, реализуем базовый абстрактный класс:

Монитор печати

Пора нам приступить к реализации монитора печати. Сперва нам понадобится нативная структура, которую мы уже описывали в теоретической части:

Теперь нам нужен интерфейс для реализации мониторов печати:

Пока что этого достаточно. В будущем, если понадобится расширять функционал не нарушая полиморфизм, благодаря нашим интерфейсам мы без проблем сможем реализовать эту задачу.

Нативный метод добавления монитора в систему мы так же уже описали в теоретическом блоке статьи, теперь опишем методы получения списка установленных мониторов, удаления мониторов и реализуем класс нашего монитора:

Теперь у нас весь необходимый функционал для работы с мониторами печати находится в одном классе. Добавим так же в нашу фабрику метод создания экземпляра монитора:

Убедимся в работоспособности нашего кода. Добавляем Unit-тест, прописываем константы имени и путей к dll монитора для удобства, реализуем тестовые методы, покрывающие основные сегменты кода:

Не забываем закинуть в тестовый каталог mfilemon.dll монитора печати.

С портом проделываем аналогичные действия. Сперва нам понадобится IPort:

Теперь описываем нативную структуру данных порта:

Spooler предусматривает два способа открытия и закрытия порта: первый — использовать базовые методы AddPort/DeletePort, второй — использовать средства XcvData и различные хэлперы над ним. Второй вариант для нас предпочтительнее, т.к. в первом случае понадобится указатель на диалоговое окно процесса установки, что нам отнюдь не нужно. Для XCV нам дополнительно понадобятся:

enum PrinterAccess - права доступа к данным принтера struct PrinterDefaults - установки принтера для XcvData struct PortData - структура данных порта для XcvData

Отлично, теперь у нас есть всё что нужно для реализации класса порта:

Теперь расширим наш PrintingApi за счёт внесения функционала для работы с портами:

Добавляем методы работы с портами в PrintingApi

Также не забываем добавить проверку в метод удаления монитора вместо TODO:


Теперь убедимся в работоспособности нашего кода и можно приступать к следующему этапу:

Драйвер принтера

С драйверами всё точно так же, как и в случае с мониторами. Сперва описываем нативную структуру данных, затем интерфейс, реализуем интерфейс, расширяем функционал API и тестируем, не забыв предварительно закинуть в тестовый каталог файлы драйверов:

Лирическое отступление :-)
Подбивают на выполнение работы одна часть которой написание драйвера принтера на COM порт.
Я развернул DDK, скомпилировал библиотеки и напоролся на засаду :-(
Великолепная книга Солдатова оказалась бесполезной.
Пошерстил интернет - не нахожу русскоязычной документации.
Есть только книга Коммисаровой, там на 3 или 4 страничках информации не стал покупать.
На сколько я понял, драйвер собой представляет DLL.
Windows берет на себя работу по рендерингу изображения через DLL идет общение с устройством и передача сгенеренного изображения. Кроме того DLL отвечает на запросы о длине/ширине печати, DPI и т.д.
Вопрос собственно короток.
Где можно почитать по идеологии построения драйвера принтер?
На безрыбье, можно и на английском языке.
В крайнем случае, может уже в сети есть отсканированная Коммисарова?
Спасибо. Странно. Всегда думал что разработка драйверов устройств всегда на совести производителя устройства. Ибо только они знают как работать со своим железом. Странно. Всегда думал что разработка драйверов устройств всегда на совести производителя устройства. Ибо только они знают как работать со своим железом.

Вы не допускаете мысли о том, что у нас могут разрабатыватся и производиться принтеры? ;-)
Как работать с железом понятно.
А как объяснить это Windows не совсем.



Ну во первых драйвер имеет расширение не dll, а sys. Хотя это и не принципиально.
А во вторых - как написать простейший драйвер, как его зарегистрировать в системе, как реагировать на прерывания и работать с устройством - все это есть у Солдатова, это уж точно.

Ну если конкретно по принтерам:
В системе на самом деле не один драйвер, а целый стек драйверов (т.е. один устанавливается над другим и т.д.). Например, сначала может идти драйвер шины, потом самого устройства, потом драйвер-филтр и еще чего нибудь. В случае принтера таких драйверов два (если я чего-то не путаю). Это parport - драйвер LPT-порта и parclass - драйвер самого устройства (кажется так они называются). Второй работает по средствам обращения к первому. Исходники обоих есть в DDK. По сути, тебе прийдется:
- Или переписать parport на свой, работающий не через LPT, а через COM;
- Или написать свой parclass, не использующий parport.
Попробуй поковыряться в этом направлении.

Хотя гораздо проще будет (если правда не требуется, чтобы принтер опредлялся в системе и сним могли работать стандартные программы):
1. Вообще отказаться от написания драйвера. Работать через стандартный драйвер COM порта. Написать библиотеку функций и все. Гемороя меньше. Ты только посмотри на исходники этих драйверов.
2. Если уж хочется все же драйвер - написать драйвер в стиле NT (как, расписано у Солдатова). Это проще.
Правда если требуется настоящий драйвер для серийной продукции, то эти два способа не подойдут.

Ну во первых драйвер имеет расширение не dll, а sys. Хотя это и не принципиально.
А во вторых - как написать простейший драйвер, как его зарегистрировать в системе, как реагировать на прерывания и работать с устройством - все это есть у Солдатова, это уж точно

Как раз принципиально ;-)

С помощью DDK компилируется dll в которой только ресурсы, собственно менюшки чтобы выбрать параметры печати.

Итак, не имея принтера, но при наличии хотя бы виртуальных принтеров можно попробовать написать программу печати из консольного приложения Windows с помощью набора функций Windows API (у меня — операционная система Windows 7 и компилятор среды «Visual Studio Community 2017», пишу на языке C++).

Функции Windows API, предназначенные для вывода на печать, там разбиты на несколько API (по ссылке есть схема, в которой нарисовано, как все эти API сочетаются между собой):

1) Print Document Package API (пока не могу его использовать, так как по ссылке написано, что этот API доступен с Windows 8 и для более поздних версий операционной системы);

2) Print Spooler API (самый нижний (близкий к железу) уровень, на нем построены все остальные перечисленные по ссылке API);

3) XPS Document API и Print Ticket API — специализированные API для печати документов в формате XPS;

4) GDI Print API — вариант, который я стал использовать. Про GDI (Graphics Device Interface, по-русски «интерфейс к графическим устройствам») подробнее можно почитать в соответствующей статье википедии. Если кратко, то с точки зрения разработчика это набор функций (входящий в Windows API), с помощью которых можно одним и тем же способом выводить информацию на любые устройства (дисплеи, принтеры, факсы и так далее) в одном и том же виде (принцип WYSIWYG).

В Windows GDI для работы с любым устройством нужно сначала получить так называемый контекст устройства (device context) в переменную-указатель типа HDC. Далее все функции Windows GDI для своей работы требуют эту переменную в качестве аргумента. Если не имеется контекста устройства (в нашем случае — принтера), то можно сначала получить название принтера, а затем по названию получить контекст устройства принтера с помощью функции CreateDC.

Как получить название принтера? Есть несколько способов:

EnumPrinters

1) Получить список принтеров с помощью функции EnumPrinters и дать пользователю выбрать нужный. Пример работы программы:

Текст программы: EnumPrinters.cpp

PrintDlg

2) Показать пользователю диалоговое окно с помощью функции PrintDlg или PrintDlgEx, в котором он выберет нужный принтер. Пример работы программы:

Тексты программ:
PrintDlg_listprn.cpp
PrintDlgEx_listprn.cpp

В случае функции PrintDlgEx диалоговое окно будет выглядеть по-другому, но принцип работы программы — тот же.

3) Указанные в предыдущем способе функции PrintDlg и PrintDlgEx можно запустить так (флаг PD_RETURNDEFAULT соответствующих структур PRINTDLG и PRINTDLGEX), что они не покажут на экране диалоговое окно со списком принтеров, а просто возвратят контекст устройства и название принтера по умолчанию. Тексты программ:
PrintDlg_defprn.cpp
PrintDlgEx_defprn.cpp

4) Ну и самый простой способ: получить название принтера по умолчанию с помощью функции GetDefaultPrinter. Текст программы: GetDefaultPrinter.cpp.

Схема вывода на печать из консольной программы:
1) Получаем название принтера (или сразу контекст устройства принтера и переходим к пункту 3). Например, одним из четырех указанных выше способов.

2) Получаем контекст устройства принтера с помощью функции CreateDC .

3) Используем функции Windows GDI, задавая им контекст устройства принтера в качестве параметра по следующей схеме:
4) Удаляем контекст устройства принтера с помощью функции DeleteDC .

Читайте также: