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.-
install python 3.7
-
pip install cffi - make sure pip is from python 3.7 if there are other
python installations on this system -
copy this source to a new file and give it a meaningful name
I gave it the name test_cffi.py -
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-
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. -
Make sure test_cffi.py is the current active document
Open the Run menu and pastecmd /k "PYTHON37_DIRECTORY\python.exe" -t -B -u $(FULL_CURRENT_PATH)
where PYTHON37_DIRECTORY needs to be changed accordingly. -
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.
- 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.
-