Przyszła pora na rozwiązanie konkursu i ukazanie kodów! :)
(technicznie pozostały jeszcze niecałe 3 godziny, no ale...)
Jedyną osobą, która przesłała rozwiązanie był @Azarien (właściwie, to przesłał mi dwa (nieco podobne) rozwiązania).
Bez zbędnego przeciągania:
Azarien #1: http://4programmers.net/Pastebin/2413
To jest o tyle ciekawe, że korzysta z zewnętrznego wywoływania printf
, więc, mimo że jest to nieco droga na około, to właśnie dzięki niej jest to interesujące podejście ;)
Azarien #2: http://4programmers.net/Pastebin/2414
Ten kod spełnia wszystkie wymogi i działa (testowane przez Azariena na FPC 2.4.0 pod x86, potwierdzone przeze mnie również we FPC 2.6.3 na x86 W8).
Niestety nie otrzymałem więcej kodów (chociaż pisał do mnie również @olesio), więc - jak obiecałem - dodam swoją wersję kodu (który nawiasem mówiąc jest podobny do wersji Azariena) i wytłumaczę o co w nim chodzi, tym samym również tłumacząc kod Azarienowy.
Kopiuj
Type pint32 = ^int32;
Procedure println(const Format: String); cdecl; external; varargs;
Procedure __internal_println(const Format: String); cdecl; [Public, Alias: '_println'];
Var Text: String = '';
Tmp : String;
Arg : Pointer;
Pos : uint16 = 1;
Len : uint16;
Begin
Arg := Pointer(uint32(@Format) + sizeof(String));
Len := Length(Format);
While (Pos <= Len) Do
Begin
if (Copy(Format, Pos, 6) = '%int32') Then
Begin
Str(pint32(Arg)^, Tmp);
Text += Tmp;
Inc(Pos, 6);
Inc(Arg, sizeof(int32));
End Else
if (Copy(Format, Pos, 6) = '%pchar') Then
Begin
Text += PPChar(Arg)^;
Inc(Pos, 6);
Inc(Arg, sizeof(PChar));
End Else
if (Copy(Format, Pos, 7) = '%double') Then
Begin
Str(PDouble(Arg)^:0:15, Tmp);
if (System.Pos('.', Tmp) > 0) Then
Begin
While (Tmp[Length(Tmp)] = '0') Do
Delete(Tmp, Length(Tmp), 1);
if (Tmp[Length(Tmp)] = '.') Then
Delete(Tmp, Length(Tmp), 1);
End;
Text += Tmp;
Inc(Pos, 7);
Inc(Arg, sizeof(Double));
End Else
Begin
Text += Format[Pos];
Inc(Pos);
End;
End;
Writeln(Text);
End;
Var A: int32 = 512;
B: PChar = 'Hello!';
C: Double = 123.456;
Begin
println('A = %int32 | B = %pchar | C = %double | 55 = %int32', A, B, C, int32(55));
End.
Zastosowałem rozwiązanie podobne do tricku, który użył @Azarien - słowem-klucz do rozwiązania tego konkursu była znajomość modyfikatora varargs
.
Jego prawdziwym zastosowaniem było ułatwienie (im)portowania funkcji z C/C++ do Pascala - w jaki sposób?
Naturalnie w C++ istnieją magiczne Variadic functions - są to funkcje, które mogą przyjąć bliżej nieskończenie dużą liczbę argumentów. Przykładem takiej najczęściej używanej funkcji jest printf
- jej definicja wygląda następująco:
Kopiuj
int printf(const char *format, ...);
Ten wielokropek oznacza, że funkcja przyjmuje jeden wymagany argument format
, a wszystkie następujące po nim są opcjonalne - mogą być dowolnego typu oraz może ich być nieskończenie wiele (lub zero).
Deklaracja tej funkcji znajduje się (w przypadku Windowsa) w bibliotece msvcrt.dll
- powstaje jednak pytanie: jak powinna wyglądać jej definicja w Pascalu (gdybyśmy ją chcieli wywołać z naszego kodu)? Przecież nie możemy po prostu napisać:
Kopiuj
Function printf(const Format: PChar; ...): Integer;
I właśnie tutaj z pomocą przychodzi varargs
:
Kopiuj
Function printf(const Format: PChar): Integer; varargs; cdecl; external 'msvcrt.dll';
To varargs
sprawia, że funkcja przyjmie po ostatnim jawnym zdefiniowanym parametrze (czyli tutaj tym Format
), dodatkowo od zera do bliżej nieskończenie wielu parametrów dodatkowych.
To była pierwsza część wymaganej wiedzy, by móc rozwiązać ten konkurs.
Lecz sprawa nie jest taka prosta - jak powiedziałem: ten keyword został zaprojektowany, by umożliwić importowanie takich C-owych printf
ów, więc wymaga on bycia połączonym z external
, nie może zostać od-tak zastosowany do jakiejkolwiek funkcji/procedury; innymi słowy, taki kod nie jest poprawny:
Kopiuj
Procedure printf(const Format: PChar); varargs;
Begin
cośtam
End;
I tutaj przychodzi druga część "wymaganej wiedzy", czyli pojęcie o istnieniu modyfikatora alias
.
Ten z kolei modyfikator umożliwia zmianę nazwy funkcji z punktu widzenia linkera - jako że kompilator zawsze* nazwę funkcji "tłumaczy" na coś bardziej linker-friendly (patrz: name mangling), czasami zajdzie potrzeba, aby funkcja/procedura miała dokładnie wyznaczoną przez nas nazwę, a nie coś w stylu ?h@@YAXH@Z
(gdy np.łączymy zewnętrzne wstawki Assemblera z naszym kodem).
Można by pomyśleć "no okej, ale jak nam to tutaj pomaga?" - otóż odpowiem, że bardzo.
Na przykładzie mojego kodu:
Kopiuj
Procedure println(const Format: String); cdecl; external; varargs;
Tą linijką informujemy kompilator, że gdzieś-tam zadeklarowana jest funkcja println
i nie musi się o nią martwić (zajmie się tym dopiero linker, z punktu widzenia kompilatora ta funkcja jako-tako 'nie istnieje' - znamy jedynie jej nagłówek, lecz nie wiemy, jak wygląda jej ciało/zawartość - identycznie, jak gdybyśmy importowali funkcję z biblioteki).
Jak widać - użyłem tutaj varargs
oraz external;
, które oznacza, że ciało tej funkcji znajduje się w tym samym pliku/projekcie, który aktualnie jest kompilowany (a będzie linkowany).
Dalej widzimy:
Kopiuj
Procedure __internal_println(const Format: String); cdecl; [Public, Alias: '_println'];
Znaczy to, że deklarujemy funkcję o nazwie w kodzie __internal_println
, która jednak po procesie kompilacji (w pliku wynikowym) nazywać się będzie _println
. Pragnąłbym zauważyć, że właśnie dwie linijki wyżej informujemy kompilator (oraz linker), że funkcji o "wewnętrznej" (z punktu widzenia linkera 'rzeczywistej') nazwie println
(a właściwie to _println
) ma oczekiwać w momencie wywoływania println(parametry);
(fachowo nazywa się to "relokacjami", gdyby ktoś nie wiedział).
Skąd jednak wziął się ten _
na początku nazwy? Cóż, po prostu println
jest na Windowsie przez FPC zamieniane na _println
w pliku wyjściowym, tak już jest. Dla porównania już w przypadku Linuxa byłoby to po prostu println
, bez dodatkowego znaku underscore
. Ot, taka konwencja.
Czyli kwestię nagłówka mamy załatwioną - pozostaje ostatni problem: jak dobrać się do parametrów?
Spójrzmy na konwencję wywoływania - jest to cdecl
, czyli parametry przekazywane są na stos od prawej do lewej.
Znaczy to tyle, że pierwszy parametr znajduje się w pamięci (na stosie) na pozycji @Format
, drugi parametr znajduje się sizeof(String)
bajtów dalej, czyli @Format+1
(lub jak ja preferuję: Pointer(uint32(@Format) + sizeof(String))
), następny znajduje się o sizeof(aktualny parametr)
bajtów dalej od tego i tak dalej. Zauważyć należy, że nie znamy typów żadnego z tych parametrów (ani nawet nie wiemy, czy rzeczywiście się tam znajdują) - przez cały czas bazujemy wyłącznie na ciągu znaków przekazanym przez użytkownika i zakładamy, że jest on poprawny (czyli nie zrobił czegoś w stylu println('x=%pchar', int32(1024));
Wobec tego w swojej funkcji najpierw przypisuję do wskaźnika adres na pierwszy parametr przekazany w wywołaniu przez programistę:
Kopiuj
Arg := Pointer(uint32(@Format) + sizeof(String));
I przesuwam ten wskaźnik odpowiednio o sizeof(uint32)
, sizeof(PChar)
lub sizeof(Double)
bajtów dalej za każdym parametrem, zgodnie z danymi zapisanymi w formacie (parametrze Format
).
@Azarien robi właściwie to samo, lecz w swojej wersji on operuje na PLongWord
(u mnie jest to zwykły Pointer
), zatem on zwiększa wartości o sizeof(uint32)/sizeof(LongWord)
, sizeof(PChar)/sizeof(LongWord)
i tak dalej.
Cóż, i to chyba tyle magii w naszych kodach :D
Prawdę mówiąc, to gdyby nie @Azarien, to sam nie znałbym tej sztuczki (w którymś z tematów - może z rok, dwa lata temu - wspomniał on o tym w jednym ze swoich postów w moim temacie i sobie to zapamiętałem - byłem ciekaw, kto jeszcze to zna ;)).
Oprócz przedstawionych przez nas rozwiązań, istnieje wciąż (przynajmniej) jedna metoda rozwiązania tego zadania-konkursu, tym razem bez korzystania z varargs
i prawie w 100% przenośnie: można stworzyć każdy wariant funkcji println
dla parametrów uint32
, PChar
i Double
, których liczba wynosi od zera do piętnastu (stąd celowo ująłem taką małą wartość w zasadach i jasno napisałem, że nie wymagane jest tworzenie żadnych dodatkowych int8
, UnicodeString
ów i całej tej reszty; myślałem, że ktoś na to wpadnie i podeśle mi taki "schizowy" kod - swoją drogą, wciąż pozostały 3 godziny, ktoś się podejmie? :D).
tl;dr
@Azarien wygrywa szacunek na całej dzielni i satysfakcję z wygranej, brawo! ;)
Jeżeli mi jeszcze jakieś ciekawe zadania wpadną na myśl, to na pewno się z Wami podzielę, a póki co... well... dzięki za udział w konkursie + mam nadzieję, że może jakiegoś programistę dzięki temu konkursowi zapoznałem/-liśmy z ciekawą varargs
-owo-alias
-ową sztuczką ;>
`*` no, prawie zawsze. Zależnie od samego kompilatora, docelowej architektury/systemu oraz zastosowanych przełączników.