【问题标题】:Why do EnumPrintersA and EnumPrintersW request the same amount of memory?为什么 EnumPrintersA 和 EnumPrintersW 请求相同数量的内存?
【发布时间】:2017-04-30 01:17:06
【问题描述】:

我使用 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;

【问题讨论】:

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


    【解决方案1】:

    我可以确认您使用 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 代码,我发现它肯定在winspool.EnumPrintersA + 0xA7 位置调用EnumPrintersW。 实际位置可能在不同的 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版本的函数也做同样的事情。 这似乎是一种避免重复代码以牺牲更大缓冲区为代价的常见机制,可能是因为无论如何都可以释放缓冲区。

    【讨论】:

    • 谢谢!有趣的是,GetDefaultPrinterW 只为默认打印机“传真”请求 4 个字节。为什么不是 8 个?
    【解决方案2】:

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

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

    main.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...
    

    太棒了(至少对我而言),您描述的行为可以重现。

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

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

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

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

      • 在右边,(Watch 1):第 1st (0th) 和最后 (6th - 因为在 2 个缓冲区的每一个的开头都有 7) 个结构

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

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

    • Memory 2上方,(Memory 1):是bufa

      的内存内容

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

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

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

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

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

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

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

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

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

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

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

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

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

    【讨论】:

    • 非常感谢!有趣的是,GetDefaultPrinterW 只为默认打印机“传真”请求 4 个字节。为什么不是 8 个?
    • 来自GetDefaultPrinter如果此参数为NULL,则函数失败并且pcchBuffer指向的变量返回所需的缓冲区大小,以字符为单位。 因此,在分配时,您必须将pcchBuffer 中第一个GetDefaultPrinter 调用返回的任何值与sizeof(TCHAR) 相乘(对于 WIDE 字符,它是 2)。
    猜你喜欢
    • 1970-01-01
    • 2022-11-23
    • 1970-01-01
    • 2019-03-22
    • 2020-05-09
    • 2017-02-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多