Community
    • Login

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

    Scheduled Pinned Locked Moved Notepad++ & Plugin Development
    1 Posts 1 Posters 985 Views
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • EkopalypseE
      Ekopalypse
      last edited by Ekopalypse

      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.

      1 Reply Last reply Reply Quote 1
      • First post
        Last post
      The Community of users of the Notepad++ text editor.
      Powered by NodeBB | Contributors