5

我使用node-ffi调用EnumPrintersA / EnumPrintersW函数来获取可从我的 PC 访问的本地打印机列表。 您应该创建将由 EnumPrinters 函数填充信息的缓冲区。 但是您不知道所需的缓冲区大小。 在这种情况下,您需要执行EnumPrintersA / EnumPrintersW两次。 在第一次调用期间,此函数计算有关打印机信息的内存量,在第二次调用期间,此函数用有关打印机的信息填充缓冲区。如果是EnumPrinters 的 Unicode 版本




功能,打印机名称中的每个字母将在 Windows 中使用两个字符进行编码。

为什么第一次调用EnumPrintersW返回与第一次调用EnumPrintersA相同的所需内存量?
Unicode 字符串的长度是非 Unicode 字符串的两倍,但所需的缓冲区大小相同。

var ffi = require('ffi')
var ref = require('ref')
var Struct = require('ref-struct')
var wchar_t = require('ref-wchar')

var int = ref.types.int
var intPtr = ref.refType(ref.types.int)
var wchar_string = wchar_t.string

var getPrintersA =  function getPrinters() {
   var PRINTER_INFO_4A = Struct({
      'pPrinterName' : ref.types.CString,
      'pServerName' : ref.types.CString,
      'Attributes' : int
   });

   var printerInfoPtr = ref.refType(PRINTER_INFO_4A);

   var winspoolLib = new ffi.Library('winspool', {
      'EnumPrintersA': [ int, [ int, ref.types.CString, int, printerInfoPtr, int, intPtr, intPtr ] ]
   });

   var pcbNeeded = ref.alloc(int, 0);
   var pcReturned = ref.alloc(int, 0);

   //Get amount of memory for the buffer with information about printers
   var res = winspoolLib.EnumPrintersA(6, ref.NULL, 4, ref.NULL, 0, pcbNeeded, pcReturned);
   if (res != 0) {
      console.log("Cannot get list of printers. Error during first call to EnumPrintersA. Error: " + res);
      return;
   }

   var bufSize = pcbNeeded.deref();
   var buf = Buffer.alloc(bufSize);

   console.log(bufSize);

   //Fill buf with information about printers
   res = winspoolLib.EnumPrintersA(6, ref.NULL, 4, buf, bufSize, pcbNeeded, pcReturned);
   if (res == 0) {
      console.log("Cannot get list of printers. Eror: " + res);
      return;
   }

   var countOfPrinters = pcReturned.deref();

   var printers = Array(countOfPrinters);
   for (var i = 0; i < countOfPrinters; i++) {
      var pPrinterInfo = ref.get(buf, i*PRINTER_INFO_4A.size, PRINTER_INFO_4A);
      printers[i] = pPrinterInfo.pPrinterName;
   }

   return printers;
};

var getPrintersW =  function getPrinters() {
   var PRINTER_INFO_4W = Struct({
      'pPrinterName' : wchar_string,
      'pServerName' : wchar_string,
      'Attributes' : int
   });

   var printerInfoPtr = ref.refType(PRINTER_INFO_4W);

   var winspoolLib = new ffi.Library('winspool', {
      'EnumPrintersW': [ int, [ int, wchar_string, int, printerInfoPtr, int, intPtr, intPtr ] ]
   });

   var pcbNeeded = ref.alloc(int, 0);
   var pcReturned = ref.alloc(int, 0);

   //Get amount of memory for the buffer with information about printers
   var res = winspoolLib.EnumPrintersW(6, ref.NULL, 4, ref.NULL, 0, pcbNeeded, pcReturned);
   if (res != 0) {
      console.log("Cannot get list of printers. Error during first call to EnumPrintersW. Eror code: " + res);
      return;
   }

   var bufSize = pcbNeeded.deref();
   var buf = Buffer.alloc(bufSize);

   console.log(bufSize);

   //Fill buf with information about printers
   res = winspoolLib.EnumPrintersW(6, ref.NULL, 4, buf, pcbNeeded.deref(), pcbNeeded, pcReturned);
   if (res == 0) {
      console.log("Cannot get list of printers. Eror code: " + res);
      return;
   }

   var countOfPrinters = pcReturned.deref();
   var printers = new Array(countOfPrinters);
   for (var i = 0; i < countOfPrinters; i++) {
      var pPrinterInfo = ref.get(buf, i*PRINTER_INFO_4W.size, PRINTER_INFO_4W);
      printers[i] = pPrinterInfo.pPrinterName;
   }

   return printers;
};

https://msdn.microsoft.com/ru-ru/library/windows/desktop/dd162692(v=vs.85).aspx

BOOL EnumPrinters(
  _In_  DWORD   Flags,
  _In_  LPTSTR  Name,
  _In_  DWORD   Level,
  _Out_ LPBYTE  pPrinterEnum,
  _In_  DWORD   cbBuf,
  _Out_ LPDWORD pcbNeeded,
  _Out_ LPDWORD pcReturned
);

https://msdn.microsoft.com/ru-ru/library/windows/desktop/dd162847(v=vs.85).aspx

typedef struct _PRINTER_INFO_4 {
  LPTSTR pPrinterName;
  LPTSTR pServerName;
  DWORD  Attributes;
} PRINTER_INFO_4, *PPRINTER_INFO_4;
4

2 回答 2

4

我可以确认您发现的EnumPrintersA内容EnumPrintersW是可重现的。在我的机器中,它们都需要 240 字节。

这让我很好奇,所以我决定为每个函数分配一个单独的缓冲区,并将每个缓冲区转储到一个文件中,然后用十六进制编辑器打开它们。每个文件的有趣部分当然是打印机的名称。

为了简短起见,我将向您展示打印机的前 3 个名称。第一行来自EnumPrintersA,第二行来自EnumPrintersW

Fax.x...FX DocuPrint C1110 PCL 6..C.1.1.1.0. .P.C.L. .6...Microsoft XPS Document Writer.o.c.u.m.e.n.t. .W.r.i.t.e.r...
F.a.x...F.X. .D.o.c.u.P.r.i.n.t. .C.1.1.1.0. .P.C.L. .6...M.i.c.r.o.s.o.f.t. .X.P.S. .D.o.c.u.m.e.n.t. .W.r.i.t.e.r...

从这个结果来看,似乎EnumPrintersA需要EnumPrintersW实际工作,然后简单地将缓冲区中的每个字符串转换为单字节字符,并将生成的字符串放在同一个位置。为了确认这一点,我决定跟踪EnumPrintersA代码,我发现它肯定EnumPrintersW在 position调用winspool.EnumPrintersA + 0xA7。在不同的 Windows 版本中,实际位置可能会有所不同。

这让我更加好奇,所以我决定测试其他有 A 和 W 版本的函数。这是我发现的:

EnumMonitorsA 280 bytes needed
EnumMonitorsW 280 bytes needed
EnumServicesStatusA 20954 bytes needed
EnumServicesStatusW 20954 bytes needed
EnumPortsA 2176 bytes needed
EnumPortsW 2176 bytes needed
EnumPrintProcessorsA 24 bytes needed
EnumPrintProcessorsW 24 bytes needed

根据这个结果,我的结论是EnumPrintersA调用EnumPrintersW实际工作并转换缓冲区中的字符串,其他具有 A 和 W 版本的函数也执行相同的操作。这似乎是一种避免重复代码以牺牲更大缓冲区为代价的常见机制,这可能是因为无论如何都可以释放缓冲区。

于 2016-12-15T03:58:21.793 回答
3

一开始我以为你的代码有问题,所以我一直在寻找错误(由FFIJS层引入,或拼写错误或类似的东西),但我找不到任何东西。

然后,我开始用C编写一个类似于你的程序(以消除任何可能引入错误的额外层)。

主.c

#include <stdio.h>
#include <Windows.h>
#include <conio.h>  // !!! Deprecated!!!


typedef BOOL (__stdcall *EnumPrintersAFuncPtr)(_In_ DWORD Flags, _In_ LPSTR Name, _In_ DWORD Level, _Out_ LPBYTE pPrinterEnum, _In_ DWORD cbBuf, _Out_ LPDWORD pcbNeeded, _Out_ LPDWORD pcReturned);
typedef BOOL (__stdcall *EnumPrintersWFuncPtr)(_In_ DWORD Flags, _In_ LPWSTR Name, _In_ DWORD Level, _Out_ LPBYTE pPrinterEnum, _In_ DWORD cbBuf, _Out_ LPDWORD pcbNeeded, _Out_ LPDWORD pcReturned);


void testFunc()
{
    PPRINTER_INFO_4A ppi4a = NULL;
    PPRINTER_INFO_4W ppi4w = NULL;
    BOOL resa, resw;
    DWORD neededa = 0, returneda = 0, neededw = 0, returnedw = 0, gle = 0, i = 0, flags = PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS;
    LPBYTE bufa = NULL, bufw = NULL;
    resa = EnumPrintersA(flags, NULL, 4, NULL, 0, &neededa, &returneda);
    if (resa) {
        printf("EnumPrintersA(1) succeeded with NULL buffer. Exiting...\n");
        return;
    } else {
        gle = GetLastError();
        if (gle != ERROR_INSUFFICIENT_BUFFER) {
            printf("EnumPrintersA(1) failed with %d(0x%08X) which is different than %d. Exiting...\n", gle, gle, ERROR_INSUFFICIENT_BUFFER);
            return;
        } else {
            printf("EnumPrintersA(1) needs a %d(0x%08X) bytes long buffer.\n", neededa, neededa);
        }
    }
    resw = EnumPrintersW(flags, NULL, 4, NULL, 0, &neededw, &returnedw);
    if (resw) {
        printf("EnumPrintersW(1) succeeded with NULL buffer. Exiting...\n");
        return;
    } else {
        gle = GetLastError();
        if (gle != ERROR_INSUFFICIENT_BUFFER) {
            printf("EnumPrintersW(1) failed with %d(0x%08X) which is different than %d. Exiting...\n", gle, gle, ERROR_INSUFFICIENT_BUFFER);
            return;
        } else {
            printf("EnumPrintersW(1) needs a %d(0x%08X) bytes long buffer.\n", neededw, neededw);
        }
    }

    bufa = (LPBYTE)calloc(1, neededa);
    if (bufa == NULL) {
        printf("calloc failed with %d(0x%08X). Exiting...\n", errno, errno);
        return;
    } else {
        printf("buffera[0x%08X:0x%08X]\n", (long)bufa, (long)bufa + neededa - 1);
    }
    bufw = (LPBYTE)calloc(1, neededw);
    if (bufw == NULL) {
        printf("calloc failed with %d(0x%08X). Exiting...\n", errno, errno);
        free(bufa);
        return;
    } else {
        printf("bufferw[0x%08X:0x%08X]\n", (long)bufw, (long)bufw + neededw - 1);
    }

    resa = EnumPrintersA(flags, NULL, 4, bufa, neededa, &neededa, &returneda);
    if (!resa) {
        gle = GetLastError();
        printf("EnumPrintersA(2) failed with %d(0x%08X). Exiting...\n", gle, gle);
        free(bufa);
        free(bufw);
        return;
    }
    printf("EnumPrintersA(2) copied %d bytes in the buffer out of which the first %d(0x%08X) represent %d structures of size %d\n", neededa, returneda * sizeof(PRINTER_INFO_4A), returneda * sizeof(PRINTER_INFO_4A), returneda, sizeof(PRINTER_INFO_4A));
    resw = EnumPrintersW(flags, NULL, 4, bufw, neededw, &neededw, &returnedw);
    if (!resw) {
        gle = GetLastError();
        printf("EnumPrintersW(2) failed with %d(0x%08X). Exiting...\n", gle, gle);
        free(bufw);
        free(bufa);
        return;
    }
    printf("EnumPrintersW(2) copied %d bytes in the buffer out of which the first %d(0x%08X) represent %d structures of size %d\n", neededw, returnedw * sizeof(PRINTER_INFO_4W), returnedw * sizeof(PRINTER_INFO_4W), returnedw, sizeof(PRINTER_INFO_4W));

    ppi4a = (PPRINTER_INFO_4A)bufa;
    ppi4w = (PPRINTER_INFO_4W)bufw;
    printf("\nPrinting ASCII results:\n");
    for (i = 0; i < returneda; i++) {
        printf("  Item %d\n    pPrinterName: [%s]\n", i, ppi4a[i].pPrinterName ? ppi4a[i].pPrinterName : "NULL");
    }
    printf("\nPrinting WIDE results:\n");
    for (i = 0; i < returnedw; i++) {
        wprintf(L"  Item %d\n    pPrinterName: [%s]\n", i, ppi4w[i].pPrinterName ? ppi4w[i].pPrinterName : L"NULL");
    }

    free(bufa);
    free(bufw);
}


int main()
{
    testFunc();
    printf("\nPress a key to exit...\n");
    getch();
    return 0;
}

注意:就变量名称而言(我将它们保持简短 - 因此不是很直观),它们名称末尾的aw意味着它们用于ASCII / WIDE版本。

最初,我担心EnumPrinters可能不会返回任何内容,因为此时我没有连接到任何打印机,但幸运的是我有一些(更准确地说是 7 个)“保存”。这是上述程序的输出(感谢@qxz 纠正了我的初始(和某种错误)版本):

EnumPrintersA(1) needs a 544(0x00000220) bytes long buffer.
EnumPrintersW(1) needs a 544(0x00000220) bytes long buffer.
buffera[0x03161B20:0x03161D3F]
bufferw[0x03165028:0x03165247]
EnumPrintersA(2) copied 544 bytes in the buffer out of which the first 84(0x00000054) represent 7 structures of size 12
EnumPrintersW(2) copied 544 bytes in the buffer out of which the first 84(0x00000054) represent 7 structures of size 12

Printing ASCII results:
  Item 0
    pPrinterName: [Send To OneNote 2013]
  Item 1
    pPrinterName: [NPI060BEF (HP LaserJet Professional M1217nfw MFP)]
  Item 2
    pPrinterName: [Microsoft XPS Document Writer]
  Item 3
    pPrinterName: [Microsoft Print to PDF]
  Item 4
    pPrinterName: [HP Universal Printing PCL 6]
  Item 5
    pPrinterName: [HP LaserJet M4345 MFP [7B63B6]]
  Item 6
    pPrinterName: [Fax]

Printing WIDE results:
  Item 0
    pPrinterName: [Send To OneNote 2013]
  Item 1
    pPrinterName: [NPI060BEF (HP LaserJet Professional M1217nfw MFP)]
  Item 2
    pPrinterName: [Microsoft XPS Document Writer]
  Item 3
    pPrinterName: [Microsoft Print to PDF]
  Item 4
    pPrinterName: [HP Universal Printing PCL 6]
  Item 5
    pPrinterName: [HP LaserJet M4345 MFP [7B63B6]]
  Item 6
    pPrinterName: [Fax]

Press a key to exit...

令人惊讶的是(至少对我而言),您描述的行为可以重现。

请注意,上面的输出来自程序的032 位编译版本(064 位指针更难阅读:)),但在为064 位构建时,行为也是可重现的(我在Win10上使用VStudio 10.0)。

由于缓冲区末尾肯定有字符串,我开始调试:

VStudio 10.0 调试窗口

上图是VStudio 10.0 Debug 窗口的图片,程序在testFunc结束时中断,就在释放第一个指针之前。现在,我不知道您对VStudio上的调试有多熟悉,所以我将介绍(相关的)窗口区域:

  • 在底部,有 2 个Watch窗口(用于在程序运行时显示变量)。如图所示,显示了变量NameValueType

    • 在右侧,(Watch 1):一个(第0)和最后一个(第 6- 因为有 7 个)位于 2 个缓冲区中的每个缓冲区的开头

    • 左边,(Watch 2):2 个缓冲区的地址

  • 在Watch窗口上方,( Memory 2 ) 是bufw的内存内容。内存窗口包含一系列行,每行中都有内存地址(左侧为灰色),后面是十六进制的内容(每个字节对应于 2 个十六进制数字 - 例如1E),然后在右侧相同的内容在字符表示中(每个字节对应于 1 个字符- 我将回到这个),然后是下一行,依此类推

  • 内存2以上,(内存1 ):是bufa的内存内容

现在,回到内存布局:并非所有右侧的char都一定是它们看起来的样子,其中一些只是为了便于阅读而显示出来。例如,右侧有很多点 ( . ),但它们并不都是点。如果您在相应的十六进制表示中寻找一个点,您会注意到其中许多是00NULL(这是一个不可打印的char,但它显示为一个点)。

关于 2 个内存窗口中的每一个(查看字符表示)的缓冲区内容,有 3 个区域:

  • PRINTER_INFO_4*区域或开头的乱码:544 字节大约对应于第 1 3行

  • 最后约 1.5行的时髦char s :它们在我们的缓冲区之外,所以我们不关心它们

  • 中间区域:存储字符串的地方

让我们看一下字符串区域(内存 2 - 中间区域):正如您所提到的,每个字符都有 2 个字节:因为在我的情况下它们都是ASCII 字符MSB(或代码页字节)始终为0(即为什么您会看到char和点交错:例如第 4 行中的“ .LaserJet ”)。

由于缓冲区中有多个字符串(或字符串,如果你愿意的话) - 甚至更好:TCHAR* 中的多个TCHAR* s -它们必须分开:这是由NULL WIDE char ( hex : 00 00 , char : " .. ") 在每个字符串的末尾;结合下一个字符串的第一个字节 (char) 也是 00 (.) 的事实看到3NULL字节的序列十六进制00 00 00char :" ..."),这是中间区域中 2 个( WIDE)字符串之间的分隔符。

现在,比较 2 个中间部分(对应于 2 个缓冲区),您会注意到字符串分隔符的位置完全相同,甚至更多:每个字符串的最后部分也相同(每个字符串的后半部分更精确)。

考虑到这一点,这是我的理论:

我认为EnumPrintersA调用EnumPrintersW,然后它遍历每个字符串(在缓冲区的末尾),并调用wcstombs甚至更好:[MS.Docs]: WideCharToMultiByte 函数对它们(将它们转换到位 - 因此生成的ASCII字符串只取WIDE字符串的前一半,第二半保持不变),而不转换所有缓冲区。我必须通过查看winspool.drv中的反汇编程序来验证这一点。

就个人而言(如果我是对的)我认为这是一个蹩脚的解决方法(或者我喜欢称之为增益),但谁知道呢,也许所有*A*W函数对(至少那些返回多个char * s in a char* ) 像这样工作。无论如何,这种方法也有优点(至少对于这两个功能):

  • dev-wise:一个函数调用另一个函数并将实现保留在一个地方是可以的(而不是在两个函数中重复它

  • 性能方面:不重新创建缓冲区是可以的,因为这意味着额外的计算;毕竟,缓冲区使用者通常不会到达缓冲区中每个ASCII字符串的后半部分

于 2016-12-15T00:40:58.210 回答