Loading lang_c_app_02...
#!/usr/bin/env python # -*- coding: utf-8 -*- #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
import sys import os import platform import ctypes import time import traceback
try: import tkinter as tk # python3 except: import Tkinter as tk # python2
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class Record: __init__=lambda self, **kw: self.__dict__.update(**kw) __repr__=lambda self: repr(self.__dict__) __eq__=lambda self, oth: oth is not None and self.__dict__==oth.__dict__ __getitem__=lambda self, attr: self.__dict__.__getitem__(attr) __setitem__=lambda self, attr, value: self.__dict__.__setitem__(attr, value) get=lambda self, attr, default=None: self.__dict__.get(attr, default)
def txt(fmt, *args, **kw): return fmt.format(*args, **kw)
def out(fmt, *args, **kw): sys.stdout.write(txt(fmt, *args, **kw))
def err(fmt, *args, **kw): sys.stderr.write(txt(fmt, *args, **kw))
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def make_screen(app): byte_count=3*app.width*app.height alignment=64 # align data on cacheline pfx1=b'P6\n' pfx2=txt('{} {}\n255\n', app.width, app.height).encode() offset=len(pfx1)+len(pfx2) app.ppm=bytearray(offset+alignment+byte_count) addr=ctypes.addressof((ctypes.c_uint8*len(app.ppm)).from_buffer(app.ppm)) adjust=(addr+offset)%alignment if adjust: adjust=alignment-adjust pfx=pfx1+(b' '*adjust)+pfx2 offset+=adjust for i in range(offset): app.ppm[i]=pfx[i] app.screen=(ctypes.c_uint8*byte_count).from_buffer(app.ppm, offset)
def event_info(app, kind, evt): (x, y, w, h, btn, key)=(app.lbl.winfo_pointerx()-app.lbl.winfo_rootx(), app.lbl.winfo_pointery()-app.lbl.winfo_rooty(), app.lbl.winfo_width(), app.lbl.winfo_height(), 0, b'') if kind==b'W': kind=b'BP' # translate wheel-up/down to button-press-4/5 btn=4 if evt.delta<0 else 5 elif kind==b'KP' or kind==b'KR': key=evt.char if evt.char and type(evt.char)==str else evt.keysym key={'\r': '\n', '\x1b': 'Escape', }.get(key, key).encode(sys.stdout.encoding) elif kind==b'BP' or kind==b'BR': btn=evt.num if kind==b'BR' and btn>3: kind=None # no release for mouse-wheel return (kind, x, y, w, h, btn, key)
def update_ppm(app): if app.ppm_convert is None: # depending on python/tkinter versions (and bugs), some conversions # are required when providing Tkinter.PhotoImage data # - python 3 wants bytes() and directly accesses them # - python 2 wants utf-8 encoded str() but decodes it to bytes again! app.ppm_convert=[] # one of these should work... if 0: # memory leak!!! app.ppm_convert.append(lambda ppm: app.top.tk._createbytearray(ppm)) if tk.__name__=='tkinter': # bad utf-8 conversions in python 2 app.ppm_convert.append(lambda ppm: bytes(ppm)) app.ppm_convert.append(lambda ppm: ppm.decode('latin-1').encode('utf-8')) app.ppm_convert.append(lambda ppm: ppm.decode('latin-1').encode('utf-8') .replace(b'\x00', b'\xc0\x80')) while app.ppm_convert: try: ppm=app.ppm_convert[0](app.ppm) # FIXME: some versions of Tkinter.PhotoImage have a memory leak! # (probably in tk._createbytearray) # thus we perform a direct call to Tcl/Tk if 0: app.photo.config(width=app.width, height=app.height, format='ppm', data=ppm) else: app.top.tk.call(app.photo.name, 'configure', '-width', app.width, '-height', app.height, '-format', 'ppm', '-data', ppm) break # this conversion was suitable except: # traceback.print_exc() del app.ppm_convert[0] # forget this conversion which was not suitable
def callback(app, kind): return lambda *evt_args: handle_event(evt_args, app, kind)
def handle_event(evt_args, app, kind): if app.must_quit: return (kind, x, y, w, h, btn, key)=event_info(app, kind, evt_args[0] if evt_args else None) if kind is None: return # ignore event if w!=app.width or h!=app.height or app.screen is None: (app.width, app.height)=(w, h) make_screen(app) now=time.time() result=app.ntv_update(kind, x, y, w, h, btn, key, app.screen, app.ntv_state) if result<0: app.must_quit=True tk._default_root.quit() else: if result&1: update_ppm(app) tk._default_root.update_idletasks() # make visible ASAP if kind==b'T': dt_ms=int(1000.0*(now+app.dt-time.time())) app.timeout_id=app.top.after(max(1, dt_ms), callback(app, b'T'))
def make_gui(app): if not tk._default_root: tk.Tk().withdraw() app.top=tk.Toplevel() app.top.title('CpPy -- %s'%sys.argv[1]) app.top.config(borderwidth=0, padx=0, pady=0) app.top.rowconfigure(0, weight=1) app.top.columnconfigure(0, weight=1) app.photo=tk.PhotoImage(width=app.width, height=app.height) app.lbl=tk.Label(app.top) app.lbl.config(width=app.width, height=app.height, anchor=tk.NW, image=app.photo) app.lbl.grid(row=0, column=0, sticky=tk.NSEW) # ugly way to force exact initial window size (w_req, h_req)=(app.width, app.height) for i in range(1000): # don't get stuck forever! tk._default_root.update() (dw, dh)=(app.lbl.winfo_width()-app.width, app.lbl.winfo_height()-app.height) if not dw and not dh: break w_req+=1 if dw<0 else (-1 if dw>0 else 0) h_req+=1 if dh<0 else (-1 if dh>0 else 0) app.lbl.config(width=w_req, height=h_req) # app.ppm_convert=None app.ppm=None app.screen=None app.top.wm_protocol('WM_DELETE_WINDOW', callback(app, b'Q')) app.top.bind('<Motion>', callback(app, b'M')) app.top.bind('<MouseWheel>', callback(app, b'W')) app.top.bind('<ButtonPress>', callback(app, b'BP')) app.top.bind('<ButtonRelease>', callback(app, b'BR')) app.top.bind('<KeyPress>', callback(app, b'KP')) app.top.bind('<KeyRelease>', callback(app, b'KR')) app.lbl.bind('<Configure>', callback(app, b'C')) app.lbl.event_generate('<Configure>', when='tail') # ensure opening is seen if app.dt>=0.0: dt_ms=int(1000.0*app.dt) app.timeout_id=app.top.after(max(1, dt_ms), callback(app, b'T'))
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def load_native_functions(app, argv): if len(argv)<2: err('usage: {} library_name [ args... ]\n', argv[0]) return False lib_name=argv[1] if sys.platform=='win32': sys_lib_name=txt('{}.dll', lib_name) elif sys.platform=='darwin': sys_lib_name=txt('lib{}.dylib', lib_name) else: sys_lib_name=txt('lib{}.so', lib_name) app.ntv_lib=None for d in [os.path.curdir, os.path.dirname(sys.modules[__name__].__file__), '']: path=os.path.abspath(os.path.join(d, sys_lib_name)) try: app.ntv_lib=ctypes.CDLL(path) break except: # traceback.print_exc() pass if app.ntv_lib is None: err('cannot load library {!r}\n', lib_name) return None for n in ('_init', '_update'): fnct_name=lib_name+n try: fnct=app.ntv_lib[fnct_name] except: traceback.print_exc() err('cannot find {!r} in {!r}\n', fnct_name, lib_name) return None app['ntv'+n]=fnct # app.ntv_init.restype=ctypes.c_void_p app.ntv_init.argtypes=[ctypes.c_int, # argc ctypes.c_void_p, # argv ctypes.c_void_p, # &inout_width ctypes.c_void_p, # &inout_height ctypes.c_void_p] # &inout_dt # app.ntv_update.restype=ctypes.c_int app.ntv_update.argtypes=[ctypes.c_char_p, # evt ctypes.c_int, # x ctypes.c_int, # y ctypes.c_int, # w ctypes.c_int, # h ctypes.c_int, # btn ctypes.c_char_p, # key ctypes.c_void_p, # screen ctypes.c_void_p] # app return True
def main(argv): app=Record() if not load_native_functions(app, argv): return 1 # args=(ctypes.c_char_p*(len(argv)+1))() for (i, a) in enumerate(argv): args[i]=a.encode(sys.stdout.encoding) if bytes!=str else a # python 2 (width, height)=(ctypes.c_int(640), ctypes.c_int(480)) dt=ctypes.c_double(-1.0) app.ntv_state=app.ntv_init(len(argv), args, ctypes.byref(width), ctypes.byref(height), ctypes.byref(dt)) if not app.ntv_state: err('cannot initialise application\n') return 1 app.width=max(1, width.value) app.height=max(1, height.value) app.dt=dt.value app.must_quit=False # make_gui(app) app.top.deiconify() app.top.mainloop() return 0
if __name__=='__main__': sys.exit(main(sys.argv))
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # this GNU-makefile relies on the GCC toolchain # nb: on Windows, the Mingw-w64 package provides the mingw32-make.exe command # ( see https://web.enib.fr/~harrouet/dev_tools/#windows.mingw )
#~~~~ control global settings ~~~~ # make opt=2 --> enable optimisation, then disable debug, but keep symbols # make opt=1 --> enable optimisation, then disable debug # make opt=0 --> disable optimisation, then enable debug opt=0 # make clang=1 --> use clang/clang++, not gcc/g++ # make clang=0 --> use gcc/g++, not clang/clang++ clang=0 # make wasm=1 --> target webassembly rather than the native platform # make wasm=0 --> target the native platform rather than webassembly wasm=0
#~~~~ build library or programs ~~~~ # if LIB_TARGET is provided, this library will be built (with its # platform-specific name), otherwise ${EXE_PREFIX}* programs will be built LIB_TARGET=NativeCode EXE_PREFIX=prog
#~~~~ detect operating system ~~~~ ifneq (${OS},Windows_NT) ifneq (,${findstring Ubuntu,${shell lsb_release -i 2>/dev/null}}) OS:=Ubuntu else ifneq (,${findstring Raspbian,${shell lsb_release -i 2>/dev/null}}) # Standard distribution for Raspberry-Pi OS:=Raspbian else OS:=${strip ${shell uname -s}} endif ifneq (,${findstring Microsoft,${shell cat /proc/version 2>/dev/null}}) # Windows-Subsystem-for-Linux OS:=${OS}_WSL endif endif
#~~~~ adjust project-specific settings ~~~~ CPPFLAGS= # CPPFLAGS+=-I header/path LDFLAGS= # LDFLAGS+=-L library/path -Wl,-rpath,library/path -l library_name CFLAGS= CXXFLAGS= BINFLAGS= ifneq (,${findstring Windows_NT,${OS}}) # nothing special for now else ifneq (,${strip ${LIB_TARGET}}) BINFLAGS+=-fPIC endif ifneq (,${findstring Darwin,${OS}}) # nothing special for now else ifneq (,${findstring Ubuntu,${OS}}) # sanitizer sometimes requires gold-linker on Ubuntu LDFLAGS+=-fuse-ld=gold else ifneq (,${findstring Raspbian,${OS}}) # some warnings may appear when mixing g++-6 and g++-7 on Raspbian CXXFLAGS+=-Wno-psabi else # nothing special for now endif endif endif
#~~~~ adjust platform-specific features (Posix/Windows/Emscripten...) ~~~~ ifneq (,${findstring Windows_NT,${OS}}) LIB_PREFIX= LIB_SUFFIX=.dll EXE_SUFFIX=.exe SKIP_LINE=echo. REMOVE=del /q else LIB_PREFIX=lib ifneq (,${findstring Darwin,${OS}}) LIB_SUFFIX=.dylib else LIB_SUFFIX=.so endif EXE_SUFFIX= SKIP_LINE=echo REMOVE=rm -rf endif ifeq (${strip ${wasm}},1) LIB_PREFIX:=lib LIB_SUFFIX:=.bc EXE_SUFFIX:=.html endif
#~~~~ deduce file names ~~~~ ifneq (,${strip ${LIB_TARGET}}) LIB_TARGET:=${LIB_PREFIX}${strip ${LIB_TARGET}}${LIB_SUFFIX} MAIN_C_FILES= MAIN_CXX_FILES= else LIB_TARGET:= MAIN_C_FILES=${wildcard ${strip ${EXE_PREFIX}}*.c} MAIN_CXX_FILES=${wildcard ${strip ${EXE_PREFIX}}*.cpp} endif COMMON_C_FILES=${filter-out ${MAIN_C_FILES},${wildcard *.c}} COMMON_CXX_FILES=${filter-out ${MAIN_CXX_FILES},${wildcard *.cpp}} # MAIN_OBJECT_FILES=${sort ${patsubst %.c,%.o,${MAIN_C_FILES}} \ ${patsubst %.cpp,%.o,${MAIN_CXX_FILES}}} COMMON_OBJECT_FILES=${sort ${patsubst %.c,%.o,${COMMON_C_FILES}} \ ${patsubst %.cpp,%.o,${COMMON_CXX_FILES}}} OBJECT_FILES=${MAIN_OBJECT_FILES} ${COMMON_OBJECT_FILES} DEPEND_FILES=${patsubst %.o,%.d,${OBJECT_FILES}} EXE_FILES=${sort ${patsubst %.c,%${EXE_SUFFIX},${MAIN_C_FILES}} \ ${patsubst %.cpp,%${EXE_SUFFIX},${MAIN_CXX_FILES}}} # GENERATED_FILES=${DEPEND_FILES} ${OBJECT_FILES} ${LIB_TARGET} ${EXE_FILES} ifneq (,${findstring Darwin,${OS}}) GENERATED_FILES+=${wildcard *.dSYM} endif ifeq (${strip ${wasm}},1) GENERATED_FILES+=${patsubst %.html,%.js,${EXE_FILES}} GENERATED_FILES+=${patsubst %.html,%.html.mem,${EXE_FILES}} GENERATED_FILES+=${patsubst %.html,%.wasm,${EXE_FILES}} GENERATED_FILES+=${patsubst %.html,%.wast,${EXE_FILES}} endif GENERATED_FILES+=${wildcard output_* *~}
#~~~~ compiler/linker settings ~~~~ CPPFLAGS+=-MMD -pedantic -Wall -Wextra -Wconversion -Wno-sign-conversion CPPFLAGS+=-Wno-unused -Wno-unused-parameter CPPFLAGS+=-Werror -Wfatal-errors CFLAGS+=-std=c99 -Wc++-compat -Wwrite-strings -Wold-style-definition -Wvla CXXFLAGS+=-std=c++17 -Wno-missing-braces LDFLAGS+= BINFLAGS+= ifeq (${strip ${wasm}},1) CC=emcc CXX=em++ CPPFLAGS+=-Wno-dollar-in-identifier-extension LDFLAGS+=-s ALLOW_MEMORY_GROWTH=1 else ifeq (${strip ${clang}},1) CC=clang CXX=clang++ else CC=gcc CXX=g++ endif # ifneq (,${strip ${MAIN_CXX_FILES} ${COMMON_CXX_FILES}}) # force c++ link if there is at least one c++ source file LD:=${CXX} else LD:=${CC} endif
#~~~~ debug/optimisation settings ~~~~ ifneq (${strip ${opt}},0) CPPFLAGS+=-DNDEBUG BINFLAGS+=-O3 -ffast-math # BINFLAGS+=-fopt-info-vec-optimized ifneq (${strip ${wasm}},1) BINFLAGS+=-march=native endif ifeq (${strip ${opt}},2) # optimise but keep symbols for profiling BINFLAGS+=-g -fno-omit-frame-pointer else BINFLAGS+=-fomit-frame-pointer endif else CPPFLAGS+=-UNDEBUG BINFLAGS+=-g -O0 ifeq (${strip ${wasm}},1) # sanitizer is not available yet with Emscripten else ifneq (,${findstring Windows_NT,${OS}}) # sanitizer is not available yet on Windows else # BINFLAGS+=-fsanitize=address,undefined ifneq (,${findstring Raspbian,${OS}}) # dynamic sanitizer libraries may not be found on Raspbian # BINFLAGS+=-static-libasan -static-libubsan endif endif endif
#~~~~ main target ~~~~ all : ${EXE_FILES} ${LIB_TARGET}
rebuild : clean all
.SUFFIXES: .SECONDARY: .PHONY: all clean rebuild
#~~~~ linker command to produce the library (if any) ~~~~ ${LIB_TARGET} : ${COMMON_OBJECT_FILES} @echo ==== linking [opt=${opt}] $@ ==== ${LD} -shared -o $@ $^ ${BINFLAGS} ${LDFLAGS} @${SKIP_LINE}
#~~~~ linker command to produce the executable files (if any) ~~~~ %${EXE_SUFFIX} : %.o ${COMMON_OBJECT_FILES} @echo ==== linking [opt=${opt}] $@ ==== ${LD} -o $@ $^ ${BINFLAGS} ${LDFLAGS} @${SKIP_LINE}
#~~~~ compiler command for every source file ~~~~ %.o : %.c @echo ==== compiling [opt=${opt}] $< ==== ${CC} -o $@ $< -c ${BINFLAGS} ${CPPFLAGS} ${CFLAGS} @${SKIP_LINE}
%.o : %.cpp @echo ==== compiling [opt=${opt}] $< ==== ${CXX} -o $@ $< -c ${BINFLAGS} ${CPPFLAGS} ${CXXFLAGS} @${SKIP_LINE}
-include ${DEPEND_FILES}
#~~~~ remove generated files ~~~~ clean : @echo ==== cleaning ==== ${REMOVE} ${GENERATED_FILES} @${SKIP_LINE}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//----------------------------------------------------------------------------
#if defined _WIN32 # define CPPY_API __declspec(dllexport) #else # define CPPY_API #endif
#include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/time.h>
#define USE_MASK 1
typedef struct { bool showFps; double fpsTime; int fpsCount; } AppState;
double // seconds (1e-6 precision) since 1970/01/01 00:00:00 UTC getTime(void) { struct timeval tv; gettimeofday(&tv, NULL); return (double)tv.tv_sec+1e-6*(double)tv.tv_usec; }
CPPY_API AppState * // application or NULL NativeCode_init(int argc, const char **argv, int *inout_width, int *inout_height, double *inout_dt) { static AppState application // the global state of the application must ={false, 0.0, 0}; // exist as long as the program does AppState *app=&application; printf("~~~~ %s() ~~~~\n", __func__); for(int i=0; i<argc; ++i) { printf("args[%d]=<%s>\n", i, argv[i]); if(strcmp(argv[i], "fps")==0) { app->showFps=true; } } const double now=getTime(); if(app->showFps) { app->fpsTime=now; *inout_dt=0.0; *inout_width=800; *inout_height=600; } else { *inout_dt=1.5; } printf("%dx%d@%g\n", *inout_width, *inout_height, *inout_dt); srand((unsigned int)(now*1e3)); return app; }
CPPY_API int // -1: quit 0: go-on 1: redraw NativeCode_update(const char *evt, int x, int y, int w, int h, int btn, const char *key, uint8_t *screen, AppState *app) { if(!app->showFps) { printf("~~~~ %s(e=%s, x=%d, y=%d, w=%d, h=%d, b=%d, k=<%s>) ~~~~\n", __func__, evt, x, y, w, h, btn, key); } if((strcmp(evt, "Q")==0)|| ((strcmp(evt, "KR")==0)&&(strcmp(key, "Escape")==0))) { return -1; // quit } if(app->showFps|| (strcmp(evt, "C")==0)|| ((strcmp(evt, "KP")==0)&&(strcmp(key, "r")==0))) { const uint8_t r1=(uint8_t)(rand()&255), g1=(uint8_t)(rand()&255), b1=(uint8_t)(rand()&255); const uint8_t r2=(uint8_t)(rand()&255), g2=(uint8_t)(rand()&255), b2=(uint8_t)(rand()&255); for(int id=0, y=0; y<h; ++y) { for(int x=0; x<w; ++x, id+=3) { const bool c=((y>>5)^(x>>5))&1; #if USE_MASK const uint8_t m1=c ? 255 : 0, m2=(uint8_t)~m1; screen[id+0]=(uint8_t)((r1&m1)|(r2&m2)); screen[id+1]=(uint8_t)((g1&m1)|(g2&m2)); screen[id+2]=(uint8_t)((b1&m1)|(b2&m2)); #else screen[id+0]=c ? r1 : r2; screen[id+1]=c ? g1 : g2; screen[id+2]=c ? b1 : b2; #endif } } if(app->showFps) { ++app->fpsCount; const double now=getTime(); const double elapsed=now-app->fpsTime; if(elapsed>2.0) { printf("%g FPS\n", app->fpsCount/elapsed); app->fpsCount=0; app->fpsTime=now; } } return 1; // redraw } return 0; // go-on }
//----------------------------------------------------------------------------
LIB_TARGET=NativeCodeEn effet, nous ne cherchons pas à fabriquer un programme exécutable autonome mais une bibliothèque dynamique nommée CppEx✍
# BINFLAGS+=-fsanitize=address,undefined
CPPY_API AppState * // application or NULL NativeCode_init(int argc, const char **argv, int *inout_width, int *inout_height, double *inout_dt)
CPPY_API int // -1: quit 0: go-on 1: redraw NativeCode_update(const char *evt, int x, int y, int w, int h, int btn, const char *key, uint8_t *screen, AppState *app)