An how to use cffi and python to build a npp plugin



  • This is a little howto build a notepad++ plugin using MOSTLY python code only.
    Mostly, because there is a little c part but first let me say thank you to
    Claudia, without here initial code and help I probably would still try to figure out
    how this could be done. Thank you.

    Building a plugin means building a dll, which means you must have something
    like Visual Studio installed and it must be found by cffi.

    This code has been tested with python3.7 and therefore it is the
    recommended python version to be used and that is already the start.

    1. install python 3.7

    2. pip install cffi - make sure pip is from python 3.7 if there are other
      python installations on this system

    3. copy this source to a new file and give it a meaningful name
      I gave it the name test_cffi.py

    4. copy the modifed plugin header file content to a file and name it nppplugin.h

    now you should have two files in your directory,
    test_cffi.py and
    nppplugin.h

    1. Modify the npp_dir variable in test_cffi.py to point to your the local npp
      installation/directory. The directory where the notepad++.exe file is.

    2. Make sure test_cffi.py is the current active document
      Open the Run menu and paste cmd /k "PYTHON37_DIRECTORY\python.exe" -t -B -u $(FULL_CURRENT_PATH)
      where PYTHON37_DIRECTORY needs to be changed accordingly.

    3. Check the output, if you see something like

       generating .\Py3Script.c
       the current directory ...
       running build_ext ...
       building 'Py3Script' ...
       creating ...
       Generating code 
       Finished generating code
     
       
       testing dll
         isUnicode is true
         plugin name is Py3Script
    

    then you are done.

    1. Start another instance and check if you see the Py3Script plugin,
      press the menu item and a new doc with a text should appear.

    source for test_cffi.py

    # -*- coding: utf-8 -*-
    import cffi
    import logging
    ffibuilder = cffi.FFI()
    ffibuilder.set_unicode(True)
    
    PLUGIN_NAME = 'Py3Script' 
    
    with open('nppplugin.h') as f:
        # read plugin.h and pass it to embedding_api(), manually
        # removing the '#' directives and the CFFI_DLLEXPORT
        data = ''.join([line for line in f if not line.startswith('#')])
        data = data.replace('CFFI_DLLEXPORT', '')
        ffibuilder.embedding_api(data)
    
    # exported python functions which will show up in the menu
    ffibuilder.cdef("""
    extern "Python" void run_code();
    """)
    
    ffibuilder.set_source(PLUGIN_NAME, r'''
    #include "nppplugin.h"
    ''')
    
    ffibuilder.embedding_init_code(
    f'from {PLUGIN_NAME} import ffi, lib'  +
    """
    import logging
    LOG_FILE = 'Py3Script.log'
    FORMAT = '%(asctime)-15s %(threadName)-10s %(thread)-5d %(funcName)-20s  %(message)s'
    logging.basicConfig(filename=LOG_FILE,
                        level=logging.INFO,
                        format=FORMAT,
                        )
    log = logging.info
    
    _keepalive = None  # to keep the c objects alive
    notif_map = {}
    npp_hwnd = None
    sci1_hwnd = None
    sci2_hwnd = None
    
    # error handler
    def _onerror(exception, exc_value, traceback):
        log(f'exception: {exception}')
        log(f'exc_value: {exc_value}')
        log(f'traceback: {traceback}')
    
    
    @ffi.def_extern(onerror=_onerror)
    def isUnicode():
        log('npp checks if this is a unicode build - always return true')
        return True
    
    
    @ffi.def_extern(onerror=_onerror)
    def getName():
        log('npp asks for the plugin name - give it to it')
        return ffi.new('TCHAR []', 'Py3Script')
    
    
    @ffi.def_extern(onerror=_onerror)
    def setInfo(npp_data):
        log('npp informs us about some window handles')
        global npp_hwnd
        global sci1_hwnd
        global sci2_hwnd
        npp_hwnd = int(ffi.cast('intptr_t', npp_data._nppHandle))
        sci1_hwnd = int(ffi.cast('intptr_t', npp_data._scintillaMainHandle))
        sci2_hwnd = int(ffi.cast('intptr_t', npp_data._scintillaSecondHandle))
        notif_map.update({npp_hwnd : 'npp',
                          sci1_hwnd : 'editor1',
                          sci2_hwnd : 'editor2'})
    
        log(f'npp hwnd is {npp_hwnd}')
        log(f'sci1 hwnd is {sci1_hwnd}')
        log(f'sci2 hwnd is {sci2_hwnd}')
    
    
    @ffi.def_extern(onerror=_onerror)
    def beNotified(notification):
        _hwndFrom = int(ffi.cast('intptr_t', notification.nmhdr.hwndFrom))
        _code = int(ffi.cast('uintptr_t', notification.nmhdr.code))
        _idFrom = int(ffi.cast('uintptr_t', notification.nmhdr.idFrom))
        if _hwndFrom in notif_map:
            log(f'from {notif_map[_hwndFrom]}')
        # else:
            # log(f'from {_hwndFrom}')
    
    
    @ffi.def_extern(onerror=_onerror)
    def messageProc(message, wparam, lparam):
        # log(f'{message} {wparam} {lparam}')
        return True
    
    
    @ffi.def_extern(onerror=_onerror)
    def getFuncsArray(_nbF):
        log('npp asks for the function/menu items')
        _nbF[0] = 1
        funcItem = ffi.new('FuncItem[1]')
        funcItem[0]._pFunc = lib.run_code
        funcItem[0]._cmdID = 0
        funcItem[0]._itemName = 'Open customized new document'
        funcItem[0]._init2Check = False
        funcItem[0]._pShKey = ffi.NULL
    
        global _keepalive
        _keepalive = funcItem
        return funcItem
    
    @ffi.def_extern(onerror=_onerror)
    def run_code():
        from ctypes import WinDLL
        SCI_LEXER = WinDLL('SciLexer.dll', use_last_error=True)
        SENDMESSAGE = WinDLL('user32', use_last_error=True).SendMessageW
    
        WM_USER = 1024
        NPPMSG = WM_USER + 1000
        NPPM_MENUCOMMAND = NPPMSG + 48
    
        IDM = 40000
        IDM_FILE = IDM + 1000
        IDM_FILE_NEW = IDM_FILE + 1
    
        result = SENDMESSAGE(npp_hwnd, NPPM_MENUCOMMAND, 0, IDM_FILE_NEW)
        log(f'npp sendmessage returned:{result}')
    
        sci_direct_function = SCI_LEXER.Scintilla_DirectFunction
        sci_direct_pointer = SENDMESSAGE(sci1_hwnd, 2185, 0, 0)
    
        SCI_ADDTEXT = 2001
        SCI_GETSTATUS = 2383
    
        message = 'this text is copyrighted by cffi :-)'.encode('utf8')
        result = sci_direct_function(sci_direct_pointer, SCI_ADDTEXT, len(message), message)
        error_status = sci_direct_function(sci_direct_pointer, SCI_GETSTATUS, None, None)
    
        if error_status > 0:
            log(f'-->> ERROR: {error_status}')
    
    """)
    
    # now this is were the magic happens
    ffibuilder.compile(target=r'Py3Script.dll', verbose=True)
    
    # this is just for testing if the created dll has the exported functions
    # and creating and copying the new plugin to the needed directory
    if __name__ == '__main__':
        from shutil import copyfile
        import ctypes
        import os
    
        npp_dir = 'D:\\PortableApps\\Npp'     # <<<< ---- THIS NEEDS TO BE MODFIED
        dll_name = PLUGIN_NAME +'.dll'
        source_dir = os.path.abspath(os.path.curdir)
        source = os.path.join(source_dir, dll_name)
        target_dir = os.path.join(npp_dir, r'plugins\Py3Script')
        target = os.path.join(target_dir, dll_name)
    
        if not os.path.exists(target_dir):
            os.makedirs(target_dir)  # create the Py3Script plugin directory
    
        if os.path.exists(target):
            os.unlink(target)  # delete an existing version - only possible if not loaded by current npp instance
    
        copyfile(source, target)
    
        # now test if the dll has the exported functions
        print('\n\ntesting dll')
        lib = ctypes.WinDLL(target, use_last_error=True)
        if lib.isUnicode():
            print('  isUnicode is true')
            print(f'  plugin name is {ctypes.wstring_at(lib.getName())}\n')
    
    

    source for nppplugin.h

    #ifndef CFFI_DLLEXPORT
    #  if defined(_MSC_VER)
    #    define CFFI_DLLEXPORT  extern __declspec(dllimport)
    #  else
    #    define CFFI_DLLEXPORT  extern
    #  endif
    #endif
    
    typedef uintptr_t uptr_t;
    typedef intptr_t sptr_t;
    typedef ptrdiff_t Sci_Position;
    
    typedef struct Sci_NotifyHeader{
    	/* Compatible with Windows NMHDR.
    	 * hwndFrom is really an environment specific window handle or pointer
    	 * but most clients of Scintilla.h do not have this type visible. */
    	void *hwndFrom;
    	uptr_t idFrom;
    	unsigned int code;
    } Sci_NotifyHeader;
    
    typedef struct SCNotification{
    	Sci_NotifyHeader nmhdr;
    	Sci_Position position;
    	/* SCN_STYLENEEDED, SCN_DOUBLECLICK, SCN_MODIFIED, SCN_MARGINCLICK, */
    	/* SCN_NEEDSHOWN, SCN_DWELLSTART, SCN_DWELLEND, SCN_CALLTIPCLICK, */
    	/* SCN_HOTSPOTCLICK, SCN_HOTSPOTDOUBLECLICK, SCN_HOTSPOTRELEASECLICK, */
    	/* SCN_INDICATORCLICK, SCN_INDICATORRELEASE, */
    	/* SCN_USERLISTSELECTION, SCN_AUTOCSELECTION */
    
    	int ch;
    	/* SCN_CHARADDED, SCN_KEY, SCN_AUTOCCOMPLETED, SCN_AUTOCSELECTION, */
    	/* SCN_USERLISTSELECTION */
    	int modifiers;
    	/* SCN_KEY, SCN_DOUBLECLICK, SCN_HOTSPOTCLICK, SCN_HOTSPOTDOUBLECLICK, */
    	/* SCN_HOTSPOTRELEASECLICK, SCN_INDICATORCLICK, SCN_INDICATORRELEASE, */
    
    	int modificationType;	/* SCN_MODIFIED */
    	const char *text;
    	/* SCN_MODIFIED, SCN_USERLISTSELECTION, SCN_AUTOCSELECTION, SCN_URIDROPPED */
    
    	Sci_Position length;		/* SCN_MODIFIED */
    	Sci_Position linesAdded;	/* SCN_MODIFIED */
    	int message;	/* SCN_MACRORECORD */
    	uptr_t wParam;	/* SCN_MACRORECORD */
    	sptr_t lParam;	/* SCN_MACRORECORD */
    	Sci_Position line;		/* SCN_MODIFIED */
    	int foldLevelNow;	/* SCN_MODIFIED */
    	int foldLevelPrev;	/* SCN_MODIFIED */
    	int margin;		/* SCN_MARGINCLICK */
    	int listType;	/* SCN_USERLISTSELECTION */
    	int x;			/* SCN_DWELLSTART, SCN_DWELLEND */
    	int y;		/* SCN_DWELLSTART, SCN_DWELLEND */
    	int token;		/* SCN_MODIFIED with SC_MOD_CONTAINER */
    	Sci_Position annotationLinesAdded;	/* SCN_MODIFIED with SC_MOD_CHANGEANNOTATION */
    	int updated;	/* SCN_UPDATEUI */
    	int listCompletionMethod;  /* SCN_AUTOCSELECTION, SCN_AUTOCCOMPLETED, SCN_USERLISTSELECTION, */
    } SCNotification;
    
    typedef const TCHAR * (__cdecl * PFUNCGETNAME)();
    
    typedef struct NppData{
    	HWND _nppHandle;
    	HWND _scintillaMainHandle;
    	HWND _scintillaSecondHandle;
    } NppData;
    
    typedef void (__cdecl * PFUNCSETINFO)(NppData);
    typedef void (__cdecl * PFUNCPLUGINCMD)(void);
    typedef void (__cdecl * PBENOTIFIED)(SCNotification *);
    typedef LRESULT (__cdecl * PMESSAGEPROC)(UINT Message, WPARAM wParam, LPARAM lParam);
    
    typedef struct ShortcutKey{
    	BOOL _isCtrl;
    	BOOL _isAlt;
    	BOOL _isShift;
    	UCHAR _key;
    }ShortcutKey;
    
    typedef struct FuncItem{
    	TCHAR _itemName[64];  // nbChar
    	PFUNCPLUGINCMD _pFunc;
    	int _cmdID;
    	BOOL _init2Check;
    	struct ShortcutKey *_pShKey;
    } FuncItem;
    
    typedef FuncItem * (__cdecl * PFUNCGETFUNCSARRAY)(int *);
    
    // You should implement (or define an empty function body) those functions which are called by Notepad++ plugin manager
    CFFI_DLLEXPORT void setInfo(NppData);
    CFFI_DLLEXPORT const TCHAR * getName();
    CFFI_DLLEXPORT FuncItem * getFuncsArray(int *nbF);
    CFFI_DLLEXPORT void beNotified(SCNotification *);
    CFFI_DLLEXPORT LRESULT messageProc(UINT Message, WPARAM wParam, LPARAM lParam);
    
    // This API return always true now, since Notepad++ isn't compiled in ANSI mode anymore
    CFFI_DLLEXPORT BOOL isUnicode();
    

    That should it be.


Log in to reply