Mit ctypes über ein Array von char-Zeigern iterieren

Python in C/C++ embedden, C-Module, ctypes, Cython, SWIG, SIP etc sind hier richtig.
Antworten
Benutzeravatar
snafu
User
Beiträge: 6750
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Kurz zum Hintergrund: Mittels curses.tigetstr() erhält man die passende Terminal-Steuersequenz, um bestimmte Befehle zu senden. Ein Beispiel:

Code: Alles auswählen

In [1]: import curses

In [2]: curses.setupterm()

In [3]: curses.tigetstr('bold')
Out[3]: '\x1b[1m'

In [4]: BOLD = curses.tigetstr('bold')

In [5]: NORMAL = curses.tigetstr('sgr0') # setzt die Einstellungen zurück

In [6]: print 'Echt {0}fett{1}'.format(BOLD, NORMAL)
------> print('Echt {0}fett{1}'.format(BOLD, NORMAL))
Echt [b]fett[/b] # sieht man im Forum nicht
Ich möchte nun in einem Dictionary für alle verfügbaren Befehle die entsprechenden Zeichenketten eintragen. Das [mod]ctypes[/mod]-Modul scheint mir da keine Hilfe zu sein, aber ein Blick in die Manpage zu tigetstr offenbart:
The tigetflag, tigetnum and tigetstr routines return the value of the capability corresponding to the terminfo capname passed to them, such as xenl.

The tigetflag routine returns the value -1 if capname is not a boolean capability, or 0 if it is canceled or absent from the terminal description.

The tigetnum routine returns the value -2 if capname is not a numeric capability, or -1 if it is canceled or absent from the terminal description.

The tigetstr routine returns the value (char *)-1 if capname is not a string capability, or 0 if it is canceled or absent from the terminal description.

The capname for each capability is given in the table column entitled capname code in the capabilities section of terminfo(5).
char *boolnames[], *boolcodes[], *boolfnames[]

char *numnames[], *numcodes[], *numfnames[]

char *strnames[], *strcodes[], *strfnames[]
These null-terminated arrays contain the capnames, the termcap codes, and the full C names, for each of the terminfo variables.
Was mich interessiert, ist also `strnames`:

Code: Alles auswählen

In [7]: from ctypes import CDLL

In [8]: from ctypes.util import find_library

In [9]: libcurses = CDLL(find_library('curses'))

In [10]: libcurses.strnames
Out[10]: <_FuncPtr object at 0x9f3e50c>
Und nun stehe ich wie Ochs' vorm Berg: Wie iteriere ich über die Namen?
Darii
User
Beiträge: 1177
Registriert: Donnerstag 29. November 2007, 17:02

Nun, die Doku sagt, dass es ein nullterminiertes Array ist. Komischer Weise gibt ctypes den Typ falsch an, also muss man vorher casten.

Code: Alles auswählen

In [1]: from ctypes import *

In [2]: from ctypes.util import find_library

In [3]: libcurses = CDLL(find_library("curses"))

In [4]: array_type = POINTER(c_char_p)

In [5]: names_array = cast(libcurses.strnames, array_type)

In [6]: from itertools import count

In [7]: for i in count():
   ...:     if names_array[i]: print names_array[i]
   ...:     else: break # Null gefunden, also Array zuende
   ...:     
   ...:     
Benutzeravatar
snafu
User
Beiträge: 6750
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Super, danke. :)

Der Weg, den du in Zeile 7 gewählt hast, kam mir auch schon in den Sinn. Würde man in C ja auch so machen. Im Prinzip hat mir nur noch der Schritt mit `cast()` gefehlt. Mir war nicht bewusst, dass das Array nochmal einen Zeiger braucht, was mir aber jetzt im Nachhinein ziemlich schlüssig erscheint. ;)

Harr, dies wäre ein hübscher Anwendungsbereich für Dictionary-Comprehensions, die ja meines Wissens als Backport in 2.7 landen sollen. (EDIT: Ich merke gerade, dass ich die letzte Aussage nicht auf Anhieb belegen kann. Sorry, wenn's nicht stimmt.)
Darii
User
Beiträge: 1177
Registriert: Donnerstag 29. November 2007, 17:02

Ich habe gerade eben sogar festgestellt, dass man eigentlich auch normal über das Array iterieren kann, man muss nur selbst abbrechen, erstaunlich wie umständlich man in C denkt, wenn man ctypes verwendet.

Code: Alles auswählen

In [6]: for name in names_array:
   ...:     if not name: break
   ...:     print name
BlackJack

Ich eliminiere ja auch gerne Indizes und hätte diese Variante anzubieten:

Code: Alles auswählen

for name in iter(iter(names_array).next, None):
    print name
Benutzeravatar
snafu
User
Beiträge: 6750
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Code: Alles auswählen

from ctypes import cast, c_char_p, CDLL, POINTER
from ctypes.util import find_library
import curses

def get_capstring_dict(terminal_name=None, setupterm=True):
    """
    Return a dictionary with all "string-producing" capability names 
    (defined by `terminfo`) and their correspondending sequences for 
    a given terminal name. If a capability string could not retrieved, 
    it will be `None`.

    If `terminal_name` is `None`, the environment variable `TERM` will 
    be used. Note, that since `setupterm()` is invoked, some side-effects 
    on `curses`-related functionality could experienced. Disabling 
    `setupterm` will avoid this and makes the caller responsible for the 
    terminal setup. Of course `terminal_name` will have no effect then.
    """
    if setupterm:
        curses.setupterm(terminal_name)
    libcurses = CDLL(find_library('curses'))
    strnames = cast(libcurses.strnames, POINTER(c_char_p))
    d = {}
    for name in strnames:
        if not name:
            return d
        d[name] = curses.tigetstr(name)

Code: Alles auswählen

dict((name, curses.tigetstr(name)) for name in strnames if name)
...erzeugt leider einen Segfault bei mir, daher die ausführlichere Variante.
BlackJack

@snafu: Naja, Du hörst halt in der "comprehension" nicht beim Nullpointer bzw `None` auf zu iterieren. Wie man das mit `iter()` machen kann, sieht man ja in meinem Beitrag.
Benutzeravatar
snafu
User
Beiträge: 6750
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Ja, stimmt. Auch wenn's für Python-Verhältnisse etwas umständlich aussieht. Dafür aber alles in einem Ausdruck.
BlackJack

@snafu: Da Nullterminierte Pointer-Arrays ja in C ab und zu mal vorkommen, könnte man das auch in eine Hilfsfunktion auslagern.
Benutzeravatar
snafu
User
Beiträge: 6750
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Ich habe jetzt `switch` eingebaut mit dem Hintergedanken, eine "Oder-Regex" aus allen Sequenzen zu schreiben, mit welcher die Terminalausgabe Treffer für Treffer durchlaufen wird. Das Ganze soll dann in XML umgewandelt werden: Steuersequenzen, die einen Modus einschalten, erzeugen ein neues Tagpaar. Wird ein weiterer Modus eingeschaltet (zum Beispiel "Unterstrichen" im Anschluss an "Fett"), möchte ich in dem Tagpaar ein weiteres erzeugen. Darin würde dann der eigentliche Text stehen (wenn er irgendwann kommt). Der Modus `NORMAL` schließt die Tagpaare ab, so dass man dann quasi auf der obersten Ebene weitermacht.

Meint ihr, sowas macht Sinn, oder ist das zu aufwändig? Ich möchte am Ende in einem speziellen WYSIWYG-Editor den Text in den selben Formatierungen anzeigen, wie es ein Emulator tun würde. Zusätzlich erhoffe ich mir davon mehr Lesbarkeit und eine Möglichkeit, solche Eigenschaften terminalunabhängig ausdrücken zu können. Praktisch ist das sicherlich auch für Anpassungen des Prompts. Hier schweben mir außerdem noch Elementnamen für "Home-Verzeichnis", "Benutzername", usw vor.

Code: Alles auswählen

from ctypes import cast, c_char_p, CDLL, POINTER
from ctypes.util import find_library
import curses
from itertools import izip

def get_capstring_dict(terminal_name=None, setupterm=True, switch=False):
    """
    Return a dictionary with all "string-producing" capability names 
    (defined by `terminfo`) and their correspondending sequences for 
    a given terminal name. If a capability string could not retrieved, 
    it will be `None`.

    If `terminal_name` is `None`, the environment variable `TERM` will 
    be used. Note, that since `setupterm()` is invoked, some side-effects 
    on `curses`-related functionality could experienced. Disabling 
    `setupterm` will avoid this and makes the caller responsible for the 
    terminal setup. Of course `terminal_name` will have no effect then.

    The default output format is:
    {capname : string}

    With `switch` enabled it is:
    {string : capname}
    """
    if setupterm:
        curses.setupterm(terminal_name)

    libcurses = CDLL(find_library('curses'))
    strnames = cast(libcurses.strnames, POINTER(c_char_p))
    capnames = iter(iter(strnames).next, None)
    strings = (curses.tigetstr(cap) for cap in capnames)

    if switch:
        return dict(izip(strings, capnames))
    return dict(izip(capnames, strings))
fred.reichbier
User
Beiträge: 155
Registriert: Freitag 29. Dezember 2006, 18:27

Hallo,

hat jetzt nichts mit dem Problem zu tun, aber ich wollte mal einwerfen, dass der Standardweg, um die exportierte Variable aus einer DLL zu bekommen, dieser hier ist:

Code: Alles auswählen

strnames = POINTER(c_char_p).in_dll(libcurses, "strnames")
Gruß,

Fred
Antworten