January 31, 2012 - Tagged as: lisp, tr.
Hiçbir motivasyonum olmaksızın Common Lisp ile uğraştığım şu günlerde(şu ana kadar kullandığım diller arasında bariz bir şekilde kullanması en zevkli olanı, bu konu hakkında bir yazı yazdım birkaç düzenlemeye hazır olur), staj projem için yaptığım çalışmalar sırasında öğrendiğim bazı şeyleri Common Lisp ile uyguluyorum. Bunlardan birisi de JVM yapısı. Common Lisp ile basit bir JVM işine giriştim. Şimdilik epey iyi gidiyorum, amacım birkaç JVM komutu(instructuion, opcode, artık ne derseniz) çalıştırabilen bir altyapı. Tüm native kütüphaneleriyle beraber Java programlarını çalıştırabilecek bir JVM yapmıyorum tabii ki.
İlk adım olarak bir Java class dosyasını okuyup, istediğim kısımlarına kolayla ulaşabileceğim bir şekilde yüklemekti. class dosyalarının yapısını şuradan inceleyebilirsiniz.
Bunu yaparken bazı kod parçalarının çok tekrar ettiğini farkettim, örneğin n byte’lık bir kısmı, bir sonraki kısımdan(bu 1 byte’lık bir veri de olabilir, tamamen farklı bir yapı da olabilir, örneğin bir interface referansı) kaç tane olduğunu bilmek için okumak. Şu şekilde birşeyler yani:
let* ((constant-count (read-bytes 2 stream))
(make-array constant-count)))
(constants (loop for i from 0 to (1- constant-count) do
(setf (elt constants i) (read-constant stream)))) (
Burda yaptığım, 2 byte okuyarak constant pool’da kaç tane sabit olduğunu öğrenmek. Buna göre kaç byte daha okuyacağıma karar vereceğim çünkü.
Tabii bir de class dosyasının istediğim kısımlarına kolayca ulaşabilmek için dosyayı farklı parçaları için structlara bölmem gerekti. Bir yerden sonra her bir struct için farklı bir okuma fonksiyonu oluşturmuştum. Ve bu okuma fonksiyonlarında da bir sürü ortak kısım vardı. Bir DSL’e çevirmeye karar verdim.
Aslında DSL ile API’ın arasındaki fark tam belli değil. Benim DSL’den kastettiğim arayüzü sunarken kendisine özel bir syntax ile sunmak. Bu yaptığım biraz da yeni birşey öğrenince hemen uygulamaya çalışma merakı aslında.
Önce nasıl kullanıldığını göstereyim, sonra macrolardan bahsedeceğim. Tüm kodu görmek isteyenler için, şu class dosyasını ayrıştıran kod, şu da DSL macroları. Tüm class dosyasını tanımladığım yapı şöyle birşey:
(defbinstruct class-file4)
(magic 2)
(minor-version 2)
(major-version
(constant-pool (:struct constant-pool))2)
(access-flags 2)
(this-class 2)
(super-class 2))
(:temp (interfaces-count 2 interfaces-count))
(interfaces (:list 2))
(:temp (fields-count
(fields (:list (:struct field) fields-count))2))
(:temp (jmethods-count
(methods (:list (:struct jmethod) jmethods-count))2))
(:temp (attributes-count (attributes (:list (:struct attribute) attributes-count)))
Tanımın yukarıda linkini verdiğim class dosyası yapısına ne kadar benzediğine dikkat edin. Şöyle çalışıyor, her defbinstruct
için bir struct
oluşturuluyor, içindeki her bir liste için gerekiyorsa(:temp
olup olmadığına göre) struct’a slot
ekleniyor. :temp
değişkenler farklı amaçlar için gerekebilir. Örneğin dosyadaki boşluklar(padding
diye geçer genelde) için, veya dosyada bir yapıdan kaç tane olduğunu okumanız gerektiğinde, ama bu değeri okuduyup oluşturduğunuz yapıya dahil etmek istemiyorsanız. Her bir defbinstruct
için bir de okuma fonksiyonu oluşturuluyor, yapının adına “read” eklenerek(burdaki örnek için read-class-file
yani).
Değişken isminden sonra gelen kısım eğer tamsayı ise, o tamsayı kadar byte okunup bu slota atanıyor, eğer (:list a b)
veya (:vector a b)
şeklinde birşeyse, a
dan b
kere okunup, liste veya vector olarak atanıyor. Eğer tamsayı kısmına (:struct a)
gibi birşey gelmişse, a
nın bir defbinstruct
ile oluşturulmuş yapı olması gerekiyor(yani read-a
diye bir fonksiyon olmalı). Bu tanımlamaların recursive bir formda olabileceğine dikkat. Şöyle birşey olabilir mesela: (field-1 (:list (:list (:struct sub-field) sub-field-count) field-count))
.
Dönüş değeri de tanımladığınız yapıdan oluşturulmuş bir struct
. Örnekteki kodda class-file-interfaces
ile interfaces alanına ulaşabilirsiniz mesela.
Okunan değerlere göre daha kompleks işler yapmanız gerektiğinde :custom
keywordu ile read
fonksiyonunu kendiniz tanımlayabilirsiniz. :custom
keywordunden sonraki kısım da yapıda olacak slotların listesi. Örneğin constant-pool
u okumak biraz daha zor(mesela double ve long sabitler constant-pool’da 2 slot kaplıyor), şöyle:
(defbinstruct constant-pool
:custom
(constants)let* ((constant-pool-count (1- (read-bytes 2 stream)))
(make-array constant-pool-count)))
(constants (loop for i from 0 to (1- constant-pool-count) do
(let ((tag (read-bytes 1 stream)))
(if (or (= tag 5) (= tag 6))
(let ((constant (if (= tag 5)
(stream)
(read-jlong stream))))
(read-jdouble setf (elt constants i) constant
(elt constants (1+ i)) constant)
(incf i))
(setf (elt constants i)
(funcall
(case tag
(1 #'read-utf-8)
(8 #'read-string-ref)
(3 #'read-jinteger)
(4 #'read-jfloat)
(7 #'read-class-ref)
(9 #'read-field-ref)
(10 #'read-method-ref)
(11 #'read-interface-method-ref)
(12 #'read-descriptor))
(stream)))))
(make-constant-pool :constants constants)))
Bu tanımladığımız read fonksiyonuna stream
diye bir parametre aktarıldığını varsayıyoruz(macro tarafından oluşturulmuş kodda aktarılıyor). Burda aslında stream yerine lexical scope ile *standard-input*
a bu stream
atanabilir. Yine de çaktırmadan *standard-input*
ile oynamak bana çok iyi bir yolmuş gibi gelmedi.
Bir başka örnek olarak da yine constant-pooldaki string sabitlerini nasıl okuduğumu göstereyim:
-8
(defbinstruct utflength 2))
(:temp (1 length))) (value (:vector
İlk 2 byte, string’in uzunluğunu veriyor. Daha sonra bu uzunluk kadar 1 byte okuyup bir vector olarak kaydediyorum.
Şimdi macrolara bakalım. İlk önce defbinstruct
kodundaki keywordleri(:vector
, :list
, :struct
) recursive olarak silip yerine gerekli Lisp kodunu ekleyen remove-keywords
macrosu:
defmacro remove-keywords (form)
(cond ((null form) '())
(integerp form)
((stream))
`(read-bytes ,form and (consp form) (keywordp (first form)))
((case (first form)
(
((:list)loop for s from 0 to (1- ,(third form))
`(second form))))
collect (remove-keywords ,(
((:vector)coerce (loop for s from 0 to (1- ,(third form))
`(second form)))
collect (remove-keywords ,(
'vector))
((:struct)intern (concatenate 'string "READ-" (string (second form)))) stream))))
`(,(t form))) (
Yaptığı şey çok basit, her :struct
keywordu gördüğü yere (read-x)
fonksiyonunu ekliyor, :list
veya :vector
gördüğü yerde de gereken loop
kodunu. İkinci olarak olarak defbinstruct
:
defmacro defbinstruct (name &body attributes)
(labels ((make-reader-name (name-symbol)
(intern (concatenate 'string "READ-" (string name-symbol)))))
(if (and (keywordp (first attributes))
(eql (first attributes) :custom))
(let ((attributes (second attributes))
(cddr attributes)))
(body (progn
`(defstruct ,name
(
,@attributes)defun ,(make-reader-name name) (stream)
(
,@body)))let ((attr-struct-names (remove-if-not #'identity
(mapcar (lambda (attr)
(unless (keywordp (first attr))
(first attr)))
(
attributes))))progn
`(defstruct ,name
(
,@attr-struct-names)defun ,(make-reader-name name) (stream)
(let* (,@(mapcar (lambda (attr)
(destructuring-bind (attr-name . bytes)
(if (keywordp (first attr))
(cons (caadr attr) (cadadr attr))
(cons (first attr) (second attr)))
(if (integerp bytes)
`(,attr-name ,(stream)
`(read-bytes ,bytes
`(remove-keywords ,bytes)))))
attributes))
intern (concatenate 'string "MAKE-" (string name)))
(,(mapcan (lambda (name) (list (intern (string name) "KEYWORD") name))
,@( attr-struct-names)))))))))
Burda da defbinstruct
altındaki listeleri gezip, oluşturulacak olan struct
a gerekli slot
ları ekliyorum ve read
fonksiyonunu oluşturuyorum. Her bir defbinstruct
için bir struct
bir de fonksiyon tanımlıyorum yani.
Kütüphane toplam 52 satır. İkinci bir örnek olarak da ID3 etiketlerini okuyacaktım ama çok kompleks geldi. Dikkat edilmesi gereken çok fazla istisna var. Aklıma daha basit bir örnek gelirse ekleyeceğim(ara ara kütüphaneyi de güncelliyorum, gistlerden takip edebilirsiniz).