为什么EnumPrintersA和EnumPrintersW请求相同数量的内存?

时间:2016-12-14 16:10:49

标签: javascript c++ node.js ffi node-ffi

我使用 node-ffi 调用 EnumPrintersA / EnumPrintersW 功能,以获取可从我的电脑访问的本地打印机列表。
你应该创建一个缓冲区,它将由EnumPrinters函数填充信息 但是你不知道缓冲区所需的大小 在这种情况下,您需要执行两次 EnumPrintersA / EnumPrintersW 。 在第一次调用期间,此函数计算有关打印机信息的内存量,在第二次调用期间,此函数使用有关打印机的信息填充缓冲区。
对于Unicode版本的 EnumPrinters 功能,打印机名称中的每个字母将使用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;

2 个答案:

答案 0 :(得分:2)

一开始我觉得你的代码出了问题,所以我一直在寻找一个错误(由 ffi js 层引入,或者是一个错字或类似的东西),但我找不到任何东西。

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

main.c中

#include <stdio.h>
#include <Windows.h>
#include <conio.h>


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;
}

注意:就变量名称而言(我保持简短 - 因此不太直观), a w 位于名称末尾表示它们用于 ASCII / WIDE 版本。

最初,我担心 EnumPrinters 可能不会返回任何内容,因为我此时没有连接到任何打印机,但幸运的是我有一些(7更准确)& #34;保存&#34 ;.这是上述程序的输出(感谢@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...

令人惊讶(至少对我而言),您所描述的行为可以被复制。

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

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

_VStudio 10.0 Debug window

上图是 VStudio 10.0 调试窗口的图片,程序在 testFunc 结尾处中断,就在释放1 st 之前指针。现在,我不知道你对 VStudio 的调试有多熟悉,所以我要走遍(相关)窗口区域:

  • 在底部,有2个 Watch 窗口(用于在程序运行时显示变量)。如图所示,显示变量 Name Value Type
    • 在右边,( Watch 1 ):1 st (0 th )和最后一个(6 th < / sup> - 因为在2个缓冲区的每一个的开头有7个结构
    • 在左侧,(观看2 ):2个缓冲区的地址
  • Watch 窗口上方,( Memory 2 )是 bufw 的内存内容。 内存窗口包含一系列行,每行包含内存地址(灰色,左侧),后面是 hex 中的内容(每个byte对应2 hex 位 - 例如 1E ),然后右侧是 char 表示中的相同内容(每个字节对应1 char - 我将返回此处),然后是下一行,依此类推
  • Memory 2 上方,( Memory 1 ):它是 bufa的内存内容

现在,回到内存布局:并非右边的所有 char 都必然是它们看起来的,其中一些只是为了人类可读性而显示。例如,右侧有很多点(),但它们不是全部点。如果您在相应的十六进制表示中查找一个点,您会注意到其中许多人 00 NULL (这是一个不可打印的 char ,但它显示为一个点)。

关于每个2 Memory 窗口的缓冲区内容(查看 char 表示),有3个区域:

  • PRINTER_INFO_4 * 区域或开头的乱码:544个字节对应大约第1个3行
  • 来自最后 ~1.5 行的时髦 char :它们在我们的缓冲区之外,所以我们不关心它们
  • 中区:存储字符串的位置

让我们看看 WIDE 字符串区域(内存2 - 中区):正如您所提到的,每个字符都有2个字节:因为在我的情况下它们都是 ASCII char MSB (或代码页字节)总是 0 (这就是为什么你看到 char s和点交错的原因:例如&#34; .LaserJet & #34;第4行)。

由于缓冲区中有多个字符串(或者字符串,如果你愿意) - 或者甚至更好:TCHAR*中的多个TCHAR* - 它们必须分开:这是由< em> NULL WIDE char hex 00 00 char :&#34; .. &#34;)在每个字符串的末尾;结合下一个字符串的1 st 字节( char )也是 00 ),您将看到一个3 NULL 字节的序列( hex 00 00 00 char :&#34; ... &#34;),这是中间区域中2( WIDE )字符串之间的分隔符。

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

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

我认为 EnumPrintersA 调用 EnumPrintersW ,然后它遍历每个字符串(在缓冲区的末尾),并调用 wcstombs 甚至更好:[MS.Docs]: WideCharToMultiByte function对它们进行转换(将它们转换到位 - 因此生成的 ASCII 字符串仅占用 WIDE的一半 st 字符串,保留2 nd 半未修改),而不转换所有缓冲区。我必须通过在 winspool.drv 中查看反汇编程序来验证这一点。

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

  • dev-wise :它的确定一个函数调用另一个函数并将实现保持在一个位置(而不是在两个函数中重复它)
  • 性能方面:它确定不重新创建缓冲区,因为那样会 意味着额外的计算;毕竟,缓冲消费者通常不会到达缓冲区中每个 ASCII 字符串的后半部分

答案 1 :(得分:2)

我可以确认您在EnumPrintersAEnumPrintersW找到的内容是可重现的。 在我的机器中,它们都需要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位置调用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版本的函数也做同样的事情。 这似乎是一种常见的机制,可以避免重复代码而不考虑更大的缓冲区,可能因为缓冲区无论如何都可以解除分配。