28 C'de kullanıcı tanımlı tipler

28 C'de kullanıcı tanımlı tipler

Bir önceki bölümde C'de yazılan yeni fonksiyonlarla Lua'ı nasıl genişleteceğimizi gördük. Şimdi, C'de yazılan yeni tiplerle Lua'ı nasıl genişleteceğimizi göreceğiz. Metametotlar ve diğer güzelliklerle bölüm boyunca ilerletilecek küçük bir örnekle başlayacağız.

Örneğimiz oldukça basit bir tiptir: Boolean dizileri. Bu örnek için temel motivasyon API konularına konsantre olmak bu yüzden karmaşık algoritmalar içermez. Bununla birlikte örnek başlı başına yararlıdır. Elbette ki Lua'da boolean dizileri uygulamak için tabloları kullanabilirsiniz. Tabi, her girişi tek bitle sakladığımız bir C uygulaması, bir tablo ile kullanılan belleğin %3'ünden daha azını kullanır.

Uygulamamız aşağıdaki tanımlamalara ihtiyaç duyacak:

#include <limits.h>

#define BITS_PER_WORD (CHAR_BIT*sizeof(unsigned int))
#define I_WORD(i) ((unsigned int)(i) / BITS_PER_WORD)
#define I_BIT(i) (1 << ((unsigned int)(i) % BITS_PER_WORD))

BITS_PER_WORD işaretsiz tamsayı cinsinden bit sayısı. Makro I_WORD, belirli bir indekse karşılık gelen bitin depolandığı word'u hesaplar ve I_BIT, bu word'un içindeki doğru bite erişmek için bir kalıbı hesaplar.

Dizilerimizi aşağıdaki yapı ile temsil edeceğiz:

typedef struct NumArray {
 int size;
 unsigned int values[1]; /* değişken kısmı */
} NumArray; 

Dizi değerlerini yer tutucu olarak yalnızca 1 büyüklüklü  olarak deklare ediyoruz çünkü C89 0 büyüklüklü diziye izin vermez; diziyi tahsis ettiğimizde gerçek büyüklüğü tanımlayacağız. Bir sonraki ifade, n öğeli bir dizi için bu büyüklüğü verir:

sizeof(NumArray) + I_WORD(n - 1)*sizeof(unsigned int)

(Orijinal yapı zaten bir öğe için alan içerdiğinden, I_WORD'E 1 eklememize gerek yok.)

28.1 KullanıcıVerisi(Userdata)

İlk endişemiz, Lua'daki NumArray yapısının nasıl temsil edileceğidir. Lua, bunun için özel olarak temel bir tip sağlar: kullanıcıverisi. Bir userdata, herhangi bir şeyi depolamak için kullanabileceğiniz, Lua'da  öntanımlı hiçbir işlem olmadan bir ham bellek alanı sunar.

lua_newuserdata fonksiyonu verilen boyutla bellek bloğu ayırır, ilgili kullanıcı verisini yığın üzerine push eder(koyar) ve blok adresini döndürür:

void *lua_newuserdata (lua_State *L, size_t size);

Herhangi bir nedenle başka yollarla bellek ayırmanız gerekiyorsa bir işaretçinin boyutuyla bir userdata oluşturmak ve gerçek bellek bloğuna bir işaretçi saklamak çok kolaydır. Bu tekniğin örneklerini bir sonraki bölümde göreceğiz.

lua_newuserdata kullanarak, yeni Boolean dizileri oluşturan fonksiyon aşağıdaki gibidir:

static int newarray(lua_State *L){
    int i, n;
    size_t nbytes;
    NumArray *a;

    n = luaL_checkint(L, 1);
    luaL_argcheck(L, n >= 1, 1, "geçersiz büyüklük");
    nbytes = sizeof(NumArray) + I_WORD(n - 1) * sizeof(unsigned int);
    a = (NumArray *)lua_newuserdata(L, nbytes);

    a->size = n;
    for (i = 0; i <= I_WORD(n - 1); i++)
        a->values[i] = 0; /* diziyi ilkle */

    return 1; /* yeni userdata artık stack'da */
}

(luaL_checkint makrosu yalnızca luaL_checkinteger için bir typecast'dır(benzer rol).). newarray Lua'da kaydedildikten sonra, a=array.new(1000) deyimi ile yeni diziler oluşturmanız mümkün olacak.

Bir girişi saklamak için, array.set(array,index,value) gibi bir çağrı kullanacağız. Daha sonra, daha geleneksel sözdizimi array[index]=value'i sürdürmek için metatabloların nasıl kullanılacağını göreceğiz. Her iki notasyon için, altta yatan işlev aynıdır. Lua her zamanki gibi 1 indeksinde başlandığını varsayar:

static int setarray(lua_State *L){
    NumArray *a = (NumArray *)lua_touserdata(L, 1);
    int index = luaL_checkint(L, 2) - 1;
    luaL_checkany(L, 3);

    luaL_argcheck(L, a != NULL, 1, "'dizi' beklenmekte");

    luaL_argcheck(L, 0 <= index && index < a->size, 2,
                  "ermin dışı indeks");

    if (lua_toboolean(L, 3))
        a->values[I_WORD(index)] |= I_BIT(index); /* biti set et */
    else
        a->values[I_WORD(index)] &= ~I_BIT(index); /* biti resetle */
    return 0;
}

Lua bir boolean için herhangi bir değeri kabul ettiğinden, üçüncü parametre için luaL_checkany kullanırız: bu , bu parametre için bir değer (herhangi bir değer) olmasını sağlar. kötü argümanlarla setarray'i çağırırsak, aydınlatıcı hata mesajları alırız:

array.set(0, 11, 0)
--> stdin:1: bad argument #1 to ’set’ (’array’ expected)
array.set(a, 0)
--> stdin:1: bad argument #3 to ’set’ (value expected)

Sonraki fonksiyon bir giriş alır:

static int getarray(lua_State *L){
    NumArray *a = (NumArray *)lua_touserdata(L, 1);
    int index = luaL_checkint(L, 2) - 1;

    luaL_argcheck(L, a != NULL, 1, "'dizi' beklenmekte");

    luaL_argcheck(L, 0 <= index && index < a->size, 2,
                  "ermin dışı indeks");

    lua_pushboolean(L, a->values[I_WORD(index)] & I_BIT(index));
    return 1;
}

Bir dizinin boyutunu almak için başka bir fonksiyon tanımlarız:

static int getsize(lua_State *L){
    NumArray *a = (NumArray *)lua_touserdata(L, 1);
    luaL_argcheck(L, a != NULL, 1, "’array’ expected");
    lua_pushinteger(L, a->size);
    return 1;
}

Son olarak, kütüphanemizi ilklememiz için bazı ekstra kodlara ihtiyacımız var:

static const struct luaL_Reg arraylib[] = {
    {"new", newarray},
    {"set", setarray},
    {"get", getarray},
    {"size", getsize},
    {NULL, NULL}
};

int luaopen_array(lua_State *L){
    luaL_register(L, "array", arraylib);
    return 1;
}

Yine, yardımcı kütüphaneden luaL_register kullanıyoruz. Verilen adla (“array” örneğimizde) bir tablo oluşturur ve arraylib dizisi ile belirtilen ad- fonksiyon çiftleri ile doldurur. 

Kütüphane açıldıktan sonra, Lua'da yeni tipimizi kullanmaya hazırız:

a = array.new(1000)
print(a) --> userdata: 0x8064d48
print(array.size(a)) --> 1000
for i=1,1000 do
   array.set(a, i, i%5 == 0)
end
print(array.get(a, 10)) --> t

28.2 Metatablolar

Mevcut uygulamamız büyük bir güvenlik açığına sahiptir. Kullanıcının  array.set(io.stdin, 1, false) gibi bir şey yazdığını varsayalım. io.stdin'daki değer bir stream((FILE*)) için bir işaretçili bir userdata. Çünkü bu bir userdata, array.set memnuniyetle geçerli bir argüman olarak onu kabul edecek; Olası sonuç bir bellek bozulması olacaktır (şansla bunun yerine ermin dışı indeks hatası alabilirsiniz). Bu tür davranışlar herhangi bir Lua Kütüphanesi için kabul edilemez. Bir C kütüphanesini nasıl kullanırsanız kullanın, verileri bozmamalı veya Lua'dan core dump üretmemelidir.

Bir tip kullanıcı verisini diğer kullanıcı verilerinden ayırt etmek için her zamanki yöntem, bu tip için benzersiz bir metatablo oluşturmaktır. Bir kullanıcı verisini her oluşturduğumuzda, karşılık gelen metatable ile işaretleriz; ve bir kullanıcı verisini her aldığımızda, doğru metatablo olup olmadığını kontrol ederiz.  Lua kodları, bir userdata'nın metatablosunu değiştiremez, çünkü kodumuz sahte olamaz.

Ayrıca, bu yeni metatabloyu depolamak için bir yere ihtiyacımız var, böylece yeni kullanıcı verisi oluşturmak ve belirli bir userdatanın doğru türde olup olmadığını kontrol etmek için ona erişebiliriz. Daha önce gördüğümüz gibi, metatablo depolamak için üç seçenek vardır: kayıt defterinde(registry), çevrede(environment) veya kütüphanedeki fonksiyonlar için bir upvalue olarak. Lua'da, yeni bir C türünü kayıt defterine kaydetmek, indeks olarak bir tip adı ve değer olarak metatablo kullanmak gelenekseldir. Diğer herhangi kayıt defteri indekslerinde olduğu gibi, çatışmaları önlemek için dikkatli bir tip adı seçmeliyiz. Örneğimizde, “LuaBook.array " adını kullanacağız yeni tip için. 

Her zamanki gibi, yardımcı kütüphane burada bize yardımcı olmak için bazı fonksiyonlar sunar. Kullanacağımız yeni yardımcı fonksiyonlar şunlar:

int luaL_newmetatable(lua_State *L, const char *tname);
void luaL_getmetatable(lua_State *L, const char *tname);
void *luaL_checkudata(lua_State *L, int index, const char *tname);

luaL_newmetatable fonksiyonu yeni bir tablo oluşturur (bir metatablo olarak kullanılmak üzere), yığının üstüne yeni tabloyu bırakır ve kayıt defterinde verilen ada tabloyu ilişkilendirir. luaL_getmetatable fonksiyonu kayıt defterinden adla ilişkilendirilmiş metatablo'u alır. Son olarak, luaL_checkudata, verilen yığın konumundaki nesnenin, verilen adla eşleşen bir metatablo ile bir userdata olup olmadığını denetler. Nesne doğru metatabloya sahip değil veya o bir kullanıcı verisi değilse bir hata yükseltir; aksi takdirde, userdata adresini döndürür.

Artık uygulamamızı başlatabiliriz. İlk adım, kütüphaneyi açan fonksiyonu değiştirmektir. Yeni sürüm, diziler için metatablo oluşturmalı:

int luaopen_array(lua_State *L){
    luaL_newmetatable(L, "LuaBook.array");
    luaL_register(L, "array", arraylib);
    return 1;
}

Sonraki adım, newarray'i değiştirmek böylece oluşturduğu tüm dizilerde bu metatabloyu set eder:

static int newarray (lua_State *L) {

    <önceki gibi>

    luaL_getmetatable(L, "LuaBook.array");
    lua_setmetatable(L, -2);

    return 1; /* yeni userdata artık stack'da */
}

lua_setmetatable fonksiyonu yığından bir tablo alır(pop eder) ve verilen indeksde nesnenin metatablosu olarak onu set eder. Bizim durumumuzda, bu nesne yeni userdata.

Son olarak, setarray, getarray ve getsize ilk argümanları olarak geçerli bir dizi olup olmadığını kontrol etmek zorunda. Görevlerini basitleştirmek için aşağıdaki makroları tanımlarız:

#define checkarray(L) \
 (NumArray *)luaL_checkudata(L, 1, "LuaBook.array")

Bu makroyu kullanarak, getsize için yeni tanımlama basittir:

static int getsize(lua_State *L){
    NumArray *a = checkarray(L);
    lua_pushinteger(L, a->size);
    return 1;
}

setarray ve getarray aynı zamanda ikinci argümanları olarak indeksi kontrol etmek için kod paylaşmakta olduğundan, aşağıdaki fonksiyonda ortak bölümlerini kısımlara ayırırız:

static unsigned int *getindex(lua_State *L, unsigned int *mask){
    NumArray *a = checkarray(L);
    int index = luaL_checkint(L, 2) - 1;

    luaL_argcheck(L, 0 <= index && index < a->size, 2, "indeks ermin dışı");

    /* öğe adresini döndür */
    *mask = I_BIT(index);
    return &a->values[I_WORD(index)];
}

getelem tanımlamasından sonra setarray ve getarray basittir:

static int setarray(lua_State *L){
    unsigned int mask;
    unsigned int *entry = getindex(L, &mask);
    luaL_checkany(L, 3);
    if (lua_toboolean(L, 3))
        *entry |= mask;
    else
        *entry &= ~mask;
    return 0;
}

static int getarray(lua_State *L){
    unsigned int mask;
    unsigned int *entry = getindex(L, &mask);
    lua_pushboolean(L, *entry & mask);
    return 1;
}

artık, array.get(io.stdin,10) gibi bir şey denerseniz, uygun bir hata mesajı alacaksınız:

error: bad argument #1 to ’getarray’ (’array’ expected)

28.3 Nesne Yönelimli Erişim

Bir sonraki adım, yeni tipimizi bir nesneye dönüştürmektir, böylece klasik nesne yönelimli sözdizimini kullanan örneklerde çalışabiliriz:

a = array.new(1000)
print(a:size()) --> 1000
a:set(10, true)
print(a:get(10)) --> true

Unutmayın ki a:size()  a.size(a)'a eşdeğerdir. Bu nedenle, bizim getsize fonksiyonunu döndürmek için  a.size ifadesini hazırlamak zorundayız. Burada anahtar mekanizma __index metametod. Tablolar için, bu metamethod, LUA belirli bir anahtar için bir değer bulamadığında çağrılır. Kullanıcı verileri için, her erişimde çağrılır, çünkü kullanıcı verilerinin hiç anahtarı yoktur.

Aşağıdaki kodu çalıştırdığımızı varsayalım:

local metaarray = getmetatable(array.new(1))
metaarray.__index = metaarray
metaarray.set = array.set
metaarray.get = array.get
metaarray.size = array.size

İlk satırda, sadece metaarray'e atadığımız metatablo'ı elde etmek için bir dizi oluşturuyoruz. (Lua'dan bir kullanıcı verisinin metatablosunu set edemeyiz, ama kısıtlama olmaksızın onun metatablosunu alabilirsiniz.) Sonra metaarray'a metaarray.__index'i set ederiz. a.size'î değerlendirdiğimizde, Lua a nesnesinde "size" anahtarını bulamaz, çünkü nesne userdata. Bu nedenle,  Lua metaarray'ın kendisi olan a metatablosunun __index alanından bu değeri almaya çalışır . Ama metaarray.size array.size'dir. boyut, böylece istediğimiz gibi a.size(a) array.size(a)'a döndürülür.

Tabii ki, aynı şeyi C'ye yazabiliriz. daha da iyisini yapabiliriz: şimdi bu diziler nesnedir, kendi operasyonlarıyla, bu işlemleri artık tablo dizisinde bulundurmamıza gerek yok. Kütüphanemizin hala dışa aktarması gereken tek fonksiyon yeni diziler oluşturacak new. Diğer tüm işlemler sadece metotlar olarak gelir. C kodu direkt olarak onları kayıt edebilir.

getsize, getarray ve setarray İşlemleri önceki yaklaşımımızdan değişmez. değişecek olan onları nasıl kaydedeceğimiz. Yani, kütüphaneyi açan fonksiyonu değiştirmeliyiz. İlk olarak, iki ayrı fonksiyon listesine, biri normal fonksiyonlar için ve biri metotlar için, ihtiyacımız var:

static const struct luaL_Reg arraylib_f [] = {
 {"new", newarray},
 {NULL, NULL}
};

static const struct luaL_Reg arraylib_m [] = {
 {"set", setarray},
 {"get", getarray},
 {"size", getsize},
 {NULL, NULL}
};

Açma fonksiyonunun yeni versiyonu lua open_array, metatablo oluşturmak, own__index alanına onu atamak, orada tüm metotları kaydetmek ve dizi tablosunu oluşturup doldurmalı:

int luaopen_array(lua_State *L){
    luaL_newmetatable(L, "LuaBook.array");

    /* metatable.__index = metatable */
    lua_pushvalue(L, -1); /*metatablo yu çoğalt */
    lua_setfield(L, -2, "__index");

    luaL_register(L, NULL, arraylib_m);

    luaL_register(L, "array", arraylib_f);
    return 1;
}

Burada lual_register'dan başka bir özellik kullanıyoruz. İlk çağrıda, kütüphane ismi olarak NULL geçtiğimizde, luaL_register , fonksiyonları paketlemek için herhangi herhangi bir tablo oluşturmaz; bunun yerine, paket tablonun yığının üstünde olduğunu varsayar. Bu örnekte, paket tablo, luaL_register'ın metotları koyacağız yer olan metatablonun kendisidir. lua_register'a bir sonraki çağrı klasik şekilde çalışır: verilen adla (array) yeni tablo yaratır ve orda verilen fonksiyonları kaydeden( bu durumda yalnızca new) .

Son bir dokunuş olarak, yeni türümüze bir __tostring metodu ekleyeceğiz, böylece print(a) array'ı artı parantez içindeki dizinin boyutunu; array(1000) gibi, yazar. Fonksiyonun kendisi burada:

int array2string (lua_State *L) {
   NumArray *a = checkarray(L);
   lua_pushfstring(L, "array(%d)", a->size);
   return 1;
}

lua_pushstring çağrısı stringi biçimlendirir ve yığın üstüne koyar. nesneler dizisinin metatablosuna dahil etmek için, arraylib_m listesine array2string'ı eklemeliyiz:

static const struct luaL_Reg arraylib_m [] = {
   {"__tostring", array2string},
   <diger metotlar>
};

28.4 Dizi Erişimi

Nesne yönelimli notasyonun alternatifi, dizilerimize erişmek için klasik dizi notasyonu kullanmaktır. a:get(i) yazmak yerine, sadece a[i] yazabiliriz. Örneğimiz için, bunu yapmak kolaydır, çünkü fonksiyonlarımız setarray ve getarray, argümanlarını metamethodlara uygun verildikleri sırayla alırlar. Hızlı bir çözüm, bu metamethodları Lua kodumuzda sağda tanımlamaktır:

local metaarray = getmetatable(array.new(1))
metaarray.__index = array.get
metaarray.__newindex = array.set
metaarray.__len = array.size

(Bu kodu, nesne yönelimli erişim için değişiklikler olmaksızın diziler için orijinal uygulama üzerinde çalıştırmalıyız.) Standart sözdizimi kullanmamız gereken tek şey şu:

a = array.new(1000)
a[10] = true -- setarray
print(a[10]) -- getarray --> true
print(#a) -- getsize --> 1000

Tercih edersek, bu metamethodları C kodumuzda kaydedebiliriz. Bunun için ilkleme fonksiyonumuzu değiştiriyoruz:

static const struct luaL_Reg arraylib_f[] = {
    {"new", newarray},
    {NULL, NULL}
};

static const struct luaL_Reg arraylib_m[] = {
    {"__newindex", setarray},
    {"__index", getarray},
    {"__len", getsize},
    {"__tostring", array2string},
    {NULL, NULL}
};

int luaopen_array(lua_State *L){
    luaL_newmetatable(L, "LuaBook.array");
    luaL_register(L, NULL, arraylib_m);
    luaL_register(L, "array", arraylib_f);
    return 1;
}

Bu yeni sürümde,  sadece bir public fonksiyon, new. Diğer tüm fonksiyonlar  yalnızca belirli işlemler için metamethodlar olarak mevcutturlar.

28.5 Hafif KullanıcıVerisi

Şimdiye kadar kullandığımız kullanıcı verileri full(dolu) userdata olarak adlandırılır.

Lua, light(hafif) userdata adı verilen başka bir kullanıcı verisi sunar. Hafif kullanıcı verisi, bir C işaretçisiyle (yani, bir void* değeri) temsil edilen bir değerdir. Bir değer olduğu için, onları yaratmıyoruz (aynı şekilde sayılarda oluşturmuyoruz). Yığına hafif userdata koymak için lua_pushlightuserdata'ı çağırırız:

void lua_pushlightuserdata (lua_State *L, void *p);

Ortak isimlere rağmen, hafif userdata ve tam userdata oldukça farklı şeylerdir. Hafif userdata tamponlar değil, tek işaretçilerdir. Metatablolara sahip değiller. Sayılar gibi, light userdata çöp toplayıcı tarafından yönetilmesine gerek yoktur ve yönetilmezde.

Bazen tam userdata için zahmetsiz bir alternatif olarak hafif userdata kullanılır. Bununla birlikte, bu tipik bir kullanım değildir. İlk olarak, hafif userdata ile hafızayı kendiniz yönetmeniz gerekir, çünkü çöp toplama işlemine tabi değildir. İkincisi, ismine rağmen, tam userdata da masrafsızdır. Verilen bellek boyutu için bir malloc ile karşılaştırıldığında çok az yük eklerler.

hafif userdata'nın gerçek kullanımı eşitlikten gelir. full userdata bir nesnedir, yalnızca kendisine eşittir. Öte yandan hafif userdata, bir C işaretçi değerini temsil eder. Bu nedenle, aynı işaretçiyi temsil eden herhangi bir kullanıcı verisine eşittir. Bu nedenle, Lua içinde C nesnelerini bulmak için light userdata kullanabilirsiniz.

Tipik bir senaryo olarak, Lua ve bir pencere sistemi arasında bir iletişim uyguladığımızı varsayalım. Bu durumda, pencereleri temsil etmek için tam userdata kullanıyoruz. Her kullanıcı datası, tüm pencere yapısı veya sistem tarafından oluşturulan bir pencere için yalnızca bir işaretçi içerebilir. Bir pencere içinde bir olay olduğunda (örneğin, bir fare tıklaması), sistem belirli bir callback çağırır ve pencereyi(kontrolu-düğme vb) adresine göre tanımlar. callback'ı Lua'ya geçirmek için, verilen pencereyi temsil eden kullanıcı verisini bulmamız gerekir. Bu userdata'yı bulmak için, indekslerin pencere adresleri ile hafif userdata ve değerlerin Lua 'daki pencereleri temsil eden tam userdata olduğu bir tablo tutabiliriz . Bir pencere adresimiz olduğunda, API yığınına hafif userdata olarak onu koyar ve bu kullanıcı verilerini o tabloda bir indeks olarak kullanırız. (Muhtemelen bu tablonun zayıf değerleri olmalıdır. Aksi takdirde, bu tam userdata asla toplanmaz.)

Hiç yorum yok:

Yorum Gönder