Functions

This document provides detailed information on how to add new functions to pywincffi and should be treated as a general guide as the implementation may vary between functions.

Adding A New Windows Function

This section walks through adding a new Windows function, WriteFile, including some of the best practices on how to handle user input. As stated at the top of this documentation, these practices will likely vary some from function to function.

Function Definition

There are two parts to defining a new function. You must define the function in Python to wrap the underlying library function and you must define the function the C header so the function can be called in Python so consider this a guide book more than a set of rules.

C Header

The C header for function definitions is located in pywincffi/core/cdefs/headers/functions.h and is sometimes referred to the ‘cdef’. When creating a new function you should essentially match what the msdn documentation defines. If you’re implementing WriteFile for example you’d look at aa365747 and copy this into functions.h as:

BOOL WINAPI WriteFile(
   _In_        HANDLE       hFile,
   _In_        LPCVOID      lpBuffer,
   _In_        DWORD        nNumberOfBytesToWrite,
   _Out_opt_   LPDWORD      lpNumberOfBytesWritten,
   _Inout_opt_ LPOVERLAPPED lpOverlapped
);

It’s important to note here that all inputs, output, optional arguments, etc are included in the header definition even if you don’t plan on exposing them from the Python wrapper.

Location of C Definitions

Currently all C definitions reside in pywincffi/core/cdefs/headers/functions.h. Unlike the Python wrapper functions, which are discussed below, the C definition is not exposed to downstream consumers. The structure of the C definition files also does not impact how the wrapper functions are structured either since both pywincffi and the downstream consumers consume from pywincffi.core.dist.load().

The C definition files could be better organized in the future if necessary. As it stands today this would only require minor changes to the HEADER_FILES and SOURCE_FILES globals in pywincffi.core.dist.

Python

Constructing The Wrapper

In order to make a Windows function available you need to write a ‘wrapper’ function. Technically speaking it’s not a requirement in order to call the underlying C function however it makes the process of calling into a C function much easier for a consumer of pywincffi.

Getting back to the WriteFile example above and the aa365747 article from msdn, WriteFile has a few input and outputs

BOOL WINAPI WriteFile(
  _In_        HANDLE       hFile,                  // input (required)
  _In_        LPCVOID      lpBuffer,               // input (required)
  _In_        DWORD        nNumberOfBytesToWrite,  // input (required)
  _Out_opt_   LPDWORD      lpNumberOfBytesWritten, // output (optional)
  _Inout_opt_ LPOVERLAPPED lpOverlapped            // input/output (optional)
);

When approaching a function like this, ask a few basic questions to compare the C implementation to Python:

  • How do you write data to a file in Python?
  • What arguments are required when you write data?
  • What do you get out of the function(s) that can write data to a file?
  • Are there functions in Python which are similar to the function being defined?

So in Python, the following input arguments are not normally required because Python typically handles them for you:

  • lpBuffer - A buffer containing the data to write
  • nNumberOfBytesToWrite - The number of bytes you intend to write

The only function which is similar to WriteFile is os.write() which takes a file descriptor and data to be written and returns the number of bytes written. So our implementation of WriteFile should be similar. In fact, it can look almost identical:

def WriteFile(hFile, lpBuffer): # -> bytes written
    pass

However since we’re wrapping a Windows function and shouldn’t artificially limit access to the underlying Windows API what should really be defined is:

def WriteFile(
    hFile, lpBuffer,
    nNumberOfBytesToWrite=None, lpOverlapped=None): # -> bytes written
    pass

Here’s how the individual arguments would be handled inside of the function:

  • hFile - A Windows handle must be created before being passed in. There is the pywincffi.kernel32.handle_from_file() function to help with going from a Python file object to Windows handle object.
  • lpBuffer - String, bytes and unicode are converted to the appropriate C type before being passed to the C call.
  • nNumberOfBytesToWrite - Can be determined from the size of lpBuffer or an integer can be provided.
  • lpOverlapped - Optional according to msdn but someone can pass in their own overlapped structure if they wanted.
Location Of Wrapper Function

For the most part what module you decide to place WriteFile in is up to you however the module should be related to the function. WriteFile is meant to operate on files so it makes sense to include it in a file module. In Windows the kernel32 library defines WriteFile so the subpackage the wrapper belongs to is also called kernel32:

pywincffi.kernel32.file.WriteFile <---- wrapper function
    ^         ^     ^
    |         |     |
  Root        |     |
 Package      |     |
        Subpackage/ |
        Windows Lib |
                    |
               Object Type
                   or
             Operation Group

New functions which come from other Windows modules should add new top level subpackages.

Import Structure

In many Python programs, full import paths are often encouraged. So to import WriteFile one would do:

from pywincffi.kernel32.file import WriteFile

Internally within pywincffi, the above import path should be used. External consumers of pywincffi would import the function like this:

from pywincffi.kernel32 import WriteFile

So when you add a new function be sure to add it to the __init__.py for the subpackage. This ensures that if the import structure has to change within one of pywincffi’s modules we’re less likely to break downstream consumers.

Argument and Keyword Naming Conventions

If an argument or keyword is intended to be an analog for an argument to a Windows API call then it should follow the same naming convention as the documented function does. The WaitForSingleObject function for example takes two arguments according to the MSDN documentation which when translated to Python would look like this:

def WaitForSingleObject(hHandle, dwMilliseconds):
    pass

Any argument or keyword which is not directly related to an input to a Windows API should instead use the standard PEP8 naming conventions:

def WaitForSingleObject(hHandle, dwMilliseconds, other_keyword=None):
    pass

Internal Variables

Like arguments or keywords variables should be named either using camelCase if they’re intended to map to a value passed into a Windows API call or using the name_with_underscores convention in other cases. Here’s an example of the two:

def UnlockFileEx(...):

     # internal variables
     ffi, library = dist.load()

     # lpOverlapped is a Windows structure
     if lpOverlapped is None:
         lpOverlapped = ffi.new("OVERLAPPED[]", [{"hEvent": hFile}])

Documentation

This section covers the basics of documenting functions in pywincffi. The below mostly applies to how Windows functions should be documented but should generally apply elsewhere in the project too.

Basic Layout

The layout of the documentation string for each function should be consistent throughout the project. This generally makes it easier to understand but also harder to miss more critical information. Below is an annotated example of a fake Windows function:

def AWindowsFunction(...):
    """
    First few sentences should tell someone what AWindowsFunction
    does.  This can usually be pulled from the MSDN documentation but
    is usually shorter and more concise.

    .. seealso::

       <url pointing to the msdn reference for AWindowsFunction>
       <url pointing to a use case or other useful information>

    :param <python type> variable_name:
        Some information about what variable_name is.  Again, can be pulled
        from the msdn documentation but should be concise as someone can
        always go read the msdn documentation.  This information should
        always state key differences, if there are any, between what
        the C api call normally expects and what the wrapper does.

    <additional keyword or argument documentation>

    :raises SomeException:
        Information about under what condition(s) SomeException may be
        raised.  SomeException should be something that's raised directly
        by AWindowsFunction.


    :rtype: <The python type returned.  Required if different from the msdn docs>
    :returns:
        Some information about the return value.  This part of the
        documentation should be excluded if the function does not
        return anything.
    """

Arguments and Keywords

Position arguments should be documented using :param <type> name: while keywords should be documented using :keyword <type> name:. The <type> is referring to the Python type rather than the Windows type which the argument may be an analog for. Here’s a simplified example:

def CreateFile(lpFileName, dwDesiredAccess, dwShareMode=None ...):
    """
    :param str lpFileName:

    :param int dwDesiredAccess:

    :keyword int dwShareMode:
    """

It’s possible to allow an input argument to support multiple types as well:

def foobar(arg1):
    """
    :type arg1: int or str
    :param arg1:
    """

If the argument or keyword you are documenting requires some additional setup, such initializing a struct, it can be helpful to include a real example:

def CreatePipe(lpPipeAttribute=None):
    """
    ...

    :keyword struct lpPipeAttributes:
        The security attributes to apply to the handle. By default
        ``NULL`` will be passed in meaning then handle we create
        cannot be inherited.  Example struct:

        >>> from pywincffi.core import dist
        >>> ffi, library = dist.load()
        >>> lpPipeAttributes = ffi.new(
        ...     "SECURITY_ATTRIBUTES[1]", [{
        ...     "nLength": ffi.sizeof("SECURITY_ATTRIBUTES"),
        ...     "bInheritHandle": True,
        ...     "lpSecurityDescriptor": ffi.NULL
        ...     }]
        ... )
    """

External References

External references, such as those referencing the msdn documentation, are usually included within a .. seealso:: block. For msdn documentation, this structure is usually preferable:

.. seealso::

   https://msdn.microsoft.com/en-us/library/<article_number>

Note

The documentation build, which is run for every commit, checks to ensure that the documents being referenced do in fact exist. If the url can’t be reached the build will fail.

Handling Input

One of the main goals of pywincffi is to provide are more Python like interface for calling Windows APIs. To do this the pywincffi functions implement type checking, conversion and argument handling so less work is necessary on the consumer’s part.

Type Checking

In order to provide better error messages and more consistent expectations of input arguments each function should perform type checking on each argument. Most type checks are run using the pywincffi.core.checks.input_check() function:

from six import integer_types
from pywincffi.core.checks import input_check

def Foobar(arg1, arg2):
    input_check("arg1", arg1, integer_types)
    input_check("arg1", arg2, allowed_values=(1, 2, 3))

If pywincffi.core.checks.input_check() does not do what you need or you have to perform multiple steps to validate an input argument you can raise the pywincffi.exceptions.InputError exception yourself.

Note

There are some enums to help with special cases (file handles, structure, etc) and more can be added. See pywincffi/core/checks.py

Type Conversion

The underlying library that pywincffi uses, cffi, can do most type conversions for you. While normally this will function as you’d expect it’s better to be explicit and handle the conversion yourself so there are fewer surprises.

Here’s an example of how an ‘automatic’ conversion would look:

library.LockFileEx(hFile, 0, 0, 0, 0, lpOverlapped)

The problem is it makes it easier to pass something into LockFileEx that cffi might not know how to convert. The error produced as a result may look strange to someone unfamiliar with cffi and it could be more difficult to debug as result.

To avoid this problem pywincffi should try to perform the cast manually before making calls to the underlying API call. This ensures that cffi shouldn’t need to do the conversion itself and limits the chance of lower level errors propagating:

library.LockFileEx(
   hFile,
   ffi.cast("DWORD", 0),
   ffi.cast("DWORD", 0),
   ffi.cast("DWORD", 0),
   ffi.cast("DWORD", 0),
   lpOverlapped
)

Keywords

In C, there’s not really an equivalent to a keyword in Python. However for many of the Windows API functions the msdn documentation may say something along the lines of This parameter can be NULL. For pywincffi, reasonable default values should be defined where possible so not every argument is always required.

As an example the lpSecurityAttributes argument for CreateFile can be NULL and would be handled like this:

def CreateFile(..., lpSecurityAttributes=None):
   ffi, library = dist.load()

   if lpSecurityAttributes is None:
      lpSecurityAttributes = ffi.NULL

Attention

Be sure that if a keyword is in fact required in some cases but not others that you raise InputError when the required keyword is not provided.

Handling Output

Many Windows functions have a return value and some return values will be stored in another variable rather returned directly from the API call. This section tries to detail a couple of different cases and how to handle them.

Windows API Error Checking

When calling a Windows function it’s the responsibility of the wrapper function in pywincffi to check for errors using the pywincffi.core.checks.error_check() function:

from pywincffi.core.checks import Enums, error_check

def WriteFile(...):
   code = library.WriteFile(
        hFile, lpBuffer, nNumberOfBytesToWrite, bytes_written, lpOverlapped)
    error_check("WriteFile", code=code, expected=Enums.NON_ZERO)

This ensures that when an API does fail pywincffi will raise a consistent error with as much information as possible to help the consumer of the API determine what the problem is.

API Return Values

If a function returns a handle, structure, etc it’s usually best to return this from the wrapper function too. Be sure the wrapper functions’s documentation provides an example if accessing or using the data requires a couple of extra steps.

Windows Constants

When it comes to Windows constants code in Python you’ll often seen one of two kinds of definitions:

FILE_ATTRIBUTE_ENCRYPTED = 0x4000  # matches the msdn reference
FILE_ATTRIBUTE_ENCRYPTED = 16384  # same as the above but turn into an int

While neither of these are incorrect there are a few problems with making constants this way:

  • It’s easy to insert a typo into a variable name or its value.
  • You have to rely on code review to check for correctness.
  • They’re not true constants and could be modified at runtime.

So in pywincffi, we usually define constants in pywincffi/core/cdefs/headers/constants.h. At compile time any typos will result in build errors and the values are replaced when the library is compiled.

Adding New Constants

To add a new constant, simply define a line in pywincffi/core/cdefs/headers/constants.h:

#define FILE_ATTRIBUTE_ENCRYPTED ...

When should new constants be defined? It varies but it’s good general practice to define all of the constants mentioned in the msdn documentation for the function you are working on. So for example if you’re working on the SetHandleInformation function the documentation at ms724935 would have you define two constants as a result:

#define HANDLE_FLAG_INHERIT ...
#define HANDLE_FLAG_PROTECT_FROM_CLOSE ...

Using Existing Constants

When developing code for pywincffi, either within the library itself or the tests, constants should be used instead of default values. To access a defined constant you’ll need to load the library:

from pywincffi.core import dist
_, library = dist.load()
library.FILE_ATTRIBUTE_ENCRYPTED