Tahun IV, Nomor 20, Juni 2002

Menulis Program dengan Teknik Dynamic Loading  

Home
Halaman Muka
 
 
 
 

 

Semua plug-ins di Netscape atau modul di Apache memakai teknik yang sama, yaitu Dynamic Loading (DL). Ada beberapa terminologi lain untuk menyebutkan teknik ini, seperti DSO (Dynamic Shared Object) atau DLL (Dynamic Link Library). Tulisan ini akan memberikan pengantar tentang bagaimana membuat program dengan memakai teknik ini. 

1.Pendahuluan 
     Dalam pembuatan sebuah program, terkadang kita tidak ingin semua fungsi diimplementasikan langsung pada program utama, karena kita mungkin belum tahu fungsi apa saja yang harus disediakan. Atau kita menginginkan pemakai program ini harus mempunyai kemungkinan untuk menambahkan fungsi-fungsi yang belum ada pada program yang akan kita tulis. Tentu saja semua itu harus dapat dilakukan tanpa harus melakukan proses rekompilasi program utama dari awal. Teknik Dynamic Loading adalah solusi yang tepat untuk merealisasikan keinginan di atas. Yang harus kita kerjakan hanyalah menulis program pokok yang mempunyai kemampuan memuatkan (loading) fungsi tambahan dan menginterpretasikan fungsi ini dengan benar. Sebelum kita membahas lebih jauh, kita harus mengetahui terlebih dahulu bagaimana tahapan proses dari file source code menjadi file yang bisa dieksekusi. Kita akan memulai dengan menulis program yang sudah tidak asing lagi bagi para pemrogram bahasa C yaitu Hello World. 

#include <stdio.h>

int main(int argc, char** argv)
{
   printf("Hello World");
}
Untuk melakukan proses kompilasi dapat dipakai perintah: 

$ gcc -o hello hello.c
     Instruksi di atas akan melakukan proses berikut: 

1.Proses kompilasi 
Source code hello.c dikompilasi menghasilkan file objek hello.o. Karena fungsi printf() tidak diimplementasikan di file ini, maka kompiler akan memberi catatan di tabel file objek ini bahwa fungsi printf() belum mempunyai referensi. 
2.Proses linking 
Linker akan mencari di librari standard pengimplementasi fungsi yang tidak dapat ditemukan pada saat proses kompilasi, dalam hal ini pengimplementasi fungsi printf(). 
     Perintah di atas secara implisit memanggil linker dan menambahkan beberapa librari standar sebagai parameter. Di sini pengimplementasi fungsi dari printf() akan ditemukan di libc yang juga ditambahkan tanpa sepengetahuan sang pemrogram. Proses linking librari ke program utama dibedakan menjadi dua tergantung dari caranya: 
2.Proses linking statis (static linking). 
Di sini jika pengimplementasi fungsi printf() ditemukan, maka akan dikopikan ke dalam file objek hello. 
3.Proses linking dinamis (dynamic linking). 
Di sini libc diimplementasikan sebagai shared library. Linker hanya akan mencatatkan referensi ke simbol pengimplementasi fungsi printf() pada file objek hello. 
     Jika file program hello hasil dari proses linking statis dieksekusi, maka objek file program akan dimuatkan ke memori. Kemudian fungsi pertama, yang nama simbolnya tergantung dari sistem (contohnya __main), akan dipanggil. Fungsi ini, yang biasanya didefinisikan oleh kompiler atau librari akan melakukan seting dan initialisasi internal dan akhirnya akan memanggil fungsi utama main() yang didefinisikan oleh user. Pada cara kedua, jika program hello dieksekusi, maka objek file ini mula-mula juga akan dimuatkan ke memori. Kemudian linker dinamis (dynamic linker) akan membaca catatan referensi yang ada di file program hello yang dihasilkan oleh linker sebelumnya. Jika objek file shared library yang berisi pengimplementasi fungsi yang dibutuhkan belum dimuatkan ke memori, maka linker dinamis akan secara rekursif memuatkan objek file shared library ini ke memori. Rekursif berarti, jika suatu file shared library mempunyai referensi ke file shared library lainnya, maka objek file shared library yang lain ini juga akan dimuatkan ke memori (jika belum dimuat) dan seterusnya. Selanjutnya semua referensi yang merujuk ke simbol pengimplementasi fungsi akan diperbaharui ke posisi simbol yang aktual. Ini disebabkan posisi simbol pengimplementasi baru dapat ditentukan setelah objek file shared library dimuatkan ke memori (runtime). Dan posisi ini selalu berubah-ubah, tergantung dari banyak hal, seperti kapan objek file shared library ini dimuatkan ke memori, objek file shared library apa saja yang sudah dimuatkan ke memori dan sebagainya. Karena kita hanya akan membahas cara yang kedua, maka untuk selanjutnya terminologi librari dimaksudkan untuk merujuk ke shared library. 
1.Shared Library 
     Seperti disebutkan di atas, setiap kali objek file librari dimuatkan ke memori tidak selalu mempunyai posisi yang sama. Maka posisi simbol di dalam file libraripun mempunyai alamat yang selalu berubah. Oleh karena itu untuk mereferensi sebuah simbol, dilakukan pengalamatan berbasis zero, yaitu pengalamatan yang relatif terhadap posisi awal dari file librari. Setelah alamat posisi awal dapat ditentukan, maka referensi semua simbol di file librari dapat dihitung pada saat waktu eksekusi (runtime). Pada Program Library HOWTO [#!Wheeler!#] disebutkan tiga jenis librari, selain librari statis (static library) dan shared library masih ada dynamic loaded library. 
     Dilihat dari arsitektur filenya, tidak ada perbedaan antara shared library dan dynamic loaded library. Pembedaan ini hanya dipandang dari sisi pemrogramnya saja. Pada shared library, proses loading dari objek file librari terjadi saat program utama dimuatkan ke memori (proses linking dinamis seperti diuraikan di atas). Sedang pada dynamic loaded library, proses loading dari file librari dapat berlangsung kapan saja selama program utama dijalankan. Proses linking ini juga sering disebut dengan runtime linking. Kita akan menamakan teknik yang memanfaatkan runtime linking ini dengan nama Dynamic Loading. Sebelum teknik ini dibahas, ada baiknya kita mengetahui aturan pemberian nama pada sebuah shared library, walaupun aturan ini tidak sepenuhnya berlaku pada dynamic loaded library. 
     Penggunaan terminologi shared library untuk selanjutnya berlaku juga pada dynamic loaded library. Berbeda dengan jenis librari statis, shared library mempunyai dua jenis nama, yaitu soname dan real name. Soname terdiri dari awalan lib, nama dari librari, kata .so diikuti tanda titik (".") dan nomor versi mayor, seperti contohnya libhello.so.1. Real name adalah nama file librari sebenarnya yang disusun dari soname ditambah tanda titik ("."), nomor minor, dan kalau ada diikuti tanda titik (".") dan nomor release, seperti contohnya libhello.so.1.1 atau libhello.so.1.1.5. Selain kedua jenis nama di atas, masih ditambahkan satu jenis nama lagi yaitu linker name yang sama dengan soname tanpa nomor versi mayor, seperti contohnya libhello.so. Program yang membutuhkan file shared library akan melakukan referensi menggunakan jenis nama ini. Secara umum linker name adalah link ke soname dan soname adalah link ke real name. 
2.API dari Dynamic Loading 
     Kebanyakan sistem operasi menyediakan API (Application Program Interface) untuk penggunaan teknik Dynamic Loading. Sayangnya, nama-nama API ini belum ada standarnya, sehingga secara umum setiap sistem operasi menggunakan nama API sendiri-sendiri. Di sini kita akan menggunakan API dari sistem operasi Linux. Sebelum kita mulai dengan contoh program, kita akan membahas terlebih dahulu fungsi-fungsi API yang ditulis dalam bahasa program C. Deklarasi dari fungsi-fungsi ini terdapat di file header <dlfcn.h>. void* dlopen(const char* filename, int flag); Fungsi ini bertujuan untuk memuatkan objek file filename ke memori. Jika pemanggilan fungsi berhasil atau file objek filename sudah ada di memori, maka fungsi akan mengembalikan sebuah handle sebagai hasilnya. Pada prinsipnya handle ini dapat dibayangkan sebagai referensi yang merujuk ke file objek filename. 
     Namun demikian handle ini tidak boleh diinterpretasikan sebagai apapun juga, tapi hanya boleh digunakan sebagai parameter waktu pemanggilan fungsi lainnya seperti dlsym() atau dlclose(). Setiap kali fungsi ini dipanggil dengan sukses, maka librari dl akan memanajemen jumlah pemakai handle ini dengan cara menginkrementasi variabel. Parameter flag menentukan bagaimana dlopen() menangani simbol yang ada di objek filename. RTLD_LAZY dan RTLD_NOW dengan kombinasi RTLD_GLOBAL atau RTLD_LOCAL dapat digunakan untuk parameter ini. Intinya, pada RTLD_LAZY relokasi objek shared library baru dilakukan pada saat referensi di shared library untuk pertama kalinya dipanggil. Sedang pada RTLD_NOW proses relokasi dari semua objek shared library yang dibutuhkan dilakukan setelah objek filename dimuatkan ke memori. Dengan RTLD_GLOBAL, maka simbol di file shared library yang dimuatkan akan dapat dipakai oleh shared library lainnya. Kalau hal ini tidak dikehendaki maka dapat dipakai nilai RTLD_LOCAL yang juga merupakan nilai default. char* dlerror(void); Dengan fungsi ini, informasi tentang sebab-sebab terjadinya kesalahan pada proses linking dinamis dapat diakses. Pada terjadinya kesalahan, fungsi ini akan mengembalikan null-terminated character string sebagai hasilnya. 
     Jika pada pemrosesan terakhir tidak terjadi kesalahan maka nilai NULL akan dikembalikan sebagai hasilnya. void* dlsym(void* handle, char* simbol); Setelah objek file filename dimuatkan ke memori, maka simbol-simbol di dalamnya dapat diakses dengan menggunakan fungsi dlsym(). Sebagai parameter pertama adalah handle yang merupakan hasil dari pemanggilan fungsi dlopen(). Parameter kedua simbol adalah nama fungsi atau nama variabel yang akan diakses. Jika simbol tidak ditemukan, maka fungsi akan mengembalikan NULL sebagai hasilnya. Pada kasus simbol ditemukan, maka sebuah pointer akan dikembalikan oleh fungsi ini sebagai hasilnya. Tergantung dari jenis simbol, pointer ini dapat diinterpretasikan sebagai pointer dari suatu fungsi atau variabel. 
     Ada hal yang perlu mendapat perhatian di sini, jika fungsi mengembalikan nilai NULL. Di sini ada dua kemungkinan, yaitu simbol tidak ditemukan seperti diuraikan di atas, atau simbol ditemukan, tetapi mempunyai nilai NULL. Untuk mendapatkan informasi yang benar, dapat dilakukan dengan cara memanggil fungsi dlerror(), setelah pemanggilan fungsi ini. Jika fungsi dlerror() mengembalikan nilai bukan NULL, maka dapat dipastikan bahwa simbol tidak ditemukan. void* dlclose(void* handle); Fungsi ini adalah kebalikan dari fungsi dlopen(). Setiap kali fungsi ini dipanggil dengan sukses, maka librari dl akan memanajemen jumlah pemakai handle ini dengan cara mendekrementasi variabel. File objek yang direferensi oleh handle baru akan dihapuskan (unload) dari memori, jika sudah tidak ada lagi yang menggunakan handle ini (jumlahnya nol). 
3.Contoh pemakaian Dynamic Loading 
     Cara pemakaian API dari Dynamic Loading akan diperlihatkan di sini dengan menggunakan program demo yang sederhana. Source code program terdiri dari dua file, yaitu main.c dan simple_dl.c (Lihat list program: script-1 adalah main.c, script-2 adalah simple_dl.c dan script-3 adalah Makefile). File main.c adalah program utama yang mempunyai fungsi main(). Sedangkan file simple_dl.c adalah file modul yang berisi simbol variabel dan simbol pengimplementasi fungsi yang akan diakses dari fungsi main() dengan menggunakan fasilitas API Dynamic Loading. Dua simbol didefinisikan di dalam simple_dl.c. Yang pertama adalah simbol variabel yang didefinisikan sebagai berikut: char* info_linux = "Info Linux!"; Nama simbol variabel ini adalah info_linux yang merupakan pointer dari tipe data char. Pada pointer ini juga secara langsung dialokasikan sejumlah memori yang berisikan karakter string "Info Linux!". Simbol kedua adalah hello_world yang merupakan simbol dari pengimplementasi fungsi hello_world(). Pada prinsipnya nama simbol tidak selalu sama dengan nama pengimplementasi fungsi. Kita akan membahas hal ini secara ringkas pada bagian akhir dari artikel ini pada kasus penulisan program menggunakan C++. Simbol fungsi ini didefinisikan sebagai berikut: void hello_world(void); Fungsi ini hanya akan menampilkan kalimat "Hello world!" di layar monitor. Sebelum kita membahas kode di file main.c, kita akan mencoba untuk mengkompilasi file simple_dl.c. Untuk dapat menghasilkan shared library, pada proses kompilasi harus digunakan parameter -fPIC. PIC adalah kepanjangan dari position independent code. Dengan instruksi berikut: 

$ gcc -fPIC -c simple_dl.c
akan dihasilkan file objek simple_dl.o. Untuk mendapatkan file shared library simple_dl.so dari file objek ini dapat digunakan instruksi: 

$ gcc -shared -o simple_dl.so simple_dl.o
     Untuk lebih yakin lagi, bahwa kedua simbol info_linux dan hello_world terdapat di file shared library simple_dl.o dapat digunakan program nm. Program ini adalah salah satu tools terdapat pada binutils yang dapat menampilkan simbol dari sebuah objek file. Instruksi 

$ nm -g --defined-only -n simple_dl.so
hanya akan menampilkan simbol global yang sudah terdefinisi. Daftar simbol akan ditampilkan secara berurutan menurut alamat relatif simbol, seperti yang terlihat di bawah ini: 

00000618 ? _init
000007a0 T hello_world
00000840 A _etext
00000840 ? _fini
00001880 D info_linux
00001898 A _GLOBAL_OFFSET_TABLE_
000018cc A _DYNAMIC
00001964 A __bss_start
00001964 A _edata
0000197c A _end
     Kolom kedua di atas menunjukkan tipe dari simbol. Huruf T mempunyai arti bahwa posisi simbol yang bersangkutan berada di bagian kode atau text, seperti halnya pada simbol hello_world yang adalah pengimplementasi fungsi. Sedangkan huruf D seperti pada simbol info_linux menandakan bahwa posisi simbol yang bersangkutan berada di bagian data yang terinitialisasi. Sekarang kita akan beralih membahas kode di dalam file main.c. Di sini kita mendefinisikan beberapa variabel. Dua diantaranya adalah void (*pFuncSimbol)(); dan char** pVarSimbol; Yang pertama adalah variabel pointer dari fungsi yang tidak mempunyai parameter (void) dan tidak mempunyai nilai kembali(void). Sedangkan variabel kedua adalah pointer dari character string. Sekarang kita akan membahas fungsi-fungsi yang dipakai di sini satu persatu. 

handle = dlopen("./simple_dl.so", RTLD_NOW);
if (!handle) {
   printf("Failed: %s\n", dlerror());
   return 1;
}
     Pertama-tama fungsi dlopen() akan dipanggil untuk memuatkan objek file shared library simple_dl.so ke memori. Program akan berakhir jika file ini tidak ditemukan. Jika objek file ini dapat dimuatkan ke memori dengan sukses, maka variabel handle selanjutnya dapat dipakai sebagai parameter untuk memanggil fungsi lainnya. 

pFuncSimbol = (void (*)())dlsym(handle, "hello_world");
error = dlerror();
if (error) {
   printf("Failed(2): %s\n", error);
   return 1;
}
     Fungsi dlsym() di atas dipanggil untuk mendapatkan referensi dari simbol hello_world. Jika simbol ini tidak ditemukan maka program akan berakhir. Informasi ini didapatkan dengan cara memanggil fungsi dlerror() yang akan mengembalikan hasil bukan NULL(lihat di atas tentang dlsym()). Jika simbol ditemukan, maka variabel pFuncSimbol sekarang mempunyai referensi ke simbol hello_world dari shared library simple_dl.so. 

pFuncSimbol();
     Dengan instruksi di atas, maka fungsi yang direferensi oleh variabel pFuncSimbol yang tak lain adalah fungsi hello_world di simple_dl.so akan dipanggil. Hasilnya dapat dilihat di layar monitor, yaitu penampilan character string "Hello world!". 

pVarSimbol = (char**)dlsym(handle, "info_linux");
error = dlerror();
if (error) {
   printf("Failed(2): %s\n", error);
   return 1;
}
     Seperti sebelumnya, di sini fungsi dlsym() dipakai untuk mendapatkan referensi dari simbol info_linux. Jika simbol ditemukan, maka sekarang pVarSimbol mempunyai referensi ke simbol info_linux yang isinya tidak lain adalah character string "Info Linux!". 

printf("%s\n", *pVarSimbol);
     Di sini isi dari referensi pVarSimbol "Info Linux!" akan ditampilkan di layar monitor. 

dlclose(handle);
     Setelah kita tidak membutuhkan lagi, maka kita harus menutup handle yang telah kita buka sebelumnya. Untuk menghasilkan file program main yang dapat dieksekusi, dapat digunakan instruksi seperti berikut: 

$ gcc -o main main.c -ldl
     Dengan parameter -ldl maka pada proses linking shared library libdl.so yang mengimplementasi API Dynamic Loading akan ditambahkan. Setelah berhasil, jika program main dieksekusi maka di layar monitor akan ada tampilan sebagai berikut: 

$ ./main
Hello world!
Info Linux!
4.Memanggil Fungsi di Program Utama dari Objek shared library 
     Pada banyak kasus kita akan memerlukan fungsi yang sudah diimplementasikan di program utama untuk dipanggil dari objek shared library. Dari sisi objek shared library sendiri tidak ada masalah, selama fungsi ini sudah terimplementasi di program utama, karena pembuatan file shared library tidak membutuhkan proses linking. Yang menjadi kunci pokok di sini adalah, bahwa simbol fungsi di program utama harus ditambahan ke tabel simbol dinamik (dynamic symbol table). Tabel ini memuat semua simbol yang akan dapat diakses oleh semua objek shared library pada saat runtime. Hal ini dapat dilakukan dengan menambahkan parameter -rdynamic pada proses pembuatan file program utama. Parameter ini akan menginstruksikan kompiler untuk melewatkan parameter -export-dynamic pada proses linking oleh ld. Jika parameter ini tidak ditambahkan, maka tabel simbol dinamik hanya akan berisi simbol yang direferensi oleh objek shared library saat proses linking pada pembuatan file program utama. Kita akan menambahkan beberapa baris pada file main.c dan simple_dl.c untuk menunjukkan hal di atas. Pada file main.c ditambahkan sebuah fungsi hello_main() yang akan dipanggil dari file simple_dl.c. 

/*==============================
 * Fungi tambahan di file main.c
 */

void hello_main(const char* txt)
{
  printf("%s\n", txt);
}
Fungsi hello_main() hanya akan menampilkan isi string txt yang dilewatkan melalui parameter. 

void hello_world(void)
{
  printf("Hello world!\n");

  /* ========================================
   * Memanggil fungsi yang ada di file main.c 
   */
  hello_main("Saya simple_dl");
}
Fungsi hello_main() di program utama akan dipanggil setelah fungsi helllo_world() ini dipanggil oleh program utama. Proses pembuatan objek shared library simple_dl.so sama seperti diterangkan sebelumnya. Pada pembuatan objek program utama, harus ditambahkan parameter -rdynamic seperti berikut: 

$ gcc -rdynamic -o main main.c -ldl
     Hasil dari pengeksekusian program main dapat dilihat pada tampilan monitor: 

$ ./main
Hello world!
Saya simple_dl
Info Linux!
5.Fungsi _init() dan _fini() 
     Fungsi _init() dan _fini() adalah dua fungsi khusus pada sebuah file shared library. Fungsi _init() akan dipanggil oleh librari dl setelah sebuah objek file shared library dimuatkan ke memori dengan instruksi dlopen(). Sedang sebelum shared library dihapuskan(unload) dari memori lewat instruksi dlclose(), librari dl akan memanggil fungsi _fini(). Kita dapat memanfaatkan kedua fungsi ini dengan cara mengimplementasikan kedua fungsi tersebut dalam shared library yang kita tulis. Pada umumnya fungsi _init() digunakan untuk menginitialisasi variabel ataupun mengalokasi memori. Fungsi _fini() sebaliknya dapat digunakan untuk menghapuskan memori yang dialokasi sebelumnya. Untuk menghindari pengimplementasian ganda, pada saat proses kompilasi pembuatan shared library harus ditambahkan parameter -nostartfiles, seperti contohnya:

$ gcc -shared -nostartfiles -o simple_dl.so simple_dl.o
6.Penggunaan C++ 
     Pada penggunaan bahasa C++ ada beberapa aspek yang perlu mendapat perhatian. Salah satu hal yang membedakan antara C++ dan C adalah, bahwa di C++ kita dapat mempunyai beberapa fungsi dengan nama yang sama tetapi parameter yang berbeda. Terminologi yang sering dipakai untuk ini pada bahasa yang mendukung object oriented adalah overloading. Lain dengan bahasa C, di sini nama fungsi saja tidak cukup untuk dijadikan sebagai identitas yang unik (unique identifier) di tabel simbol. Untuk mendapatkan identitas yang unik ini, akan dilakukan proses mangling terhadap nama fungsi di C++ yang menyertakan informasi parameternya. Misalnya saja dengan memakai gcc, simbol fungsi foobar dengan parameter tunggal int akan berubah menjadi foobar__Fi. Proses mangling ini bergantung kepada jenis kompiler, sehingga pada pemakaian kompiler yang berbeda akan dihasilkan identitas unik yang berbeda juga. Dengan menggunakan program c++filt yang juga terdapat dalam paket binutils, kita dapat melakukan proses demangling, yaitu proses kebalikan dari mangling. Contohnya dengan intruksi 

c++filt foobar__Fi
maka dari simbol foobar__Fi kita akan mendapatkan kembali nama fungsi dengan parameternya foobar(int). Hal di atas akan membuat masalah pada pembuatan program yang memakai bahasa C dan C++, karena simbol fungsi program yang ditulis dengan C juga akan dilakukan proses mangling. 
     Sebagai contoh kita ingin menulis program yang memakai librari dari pihak ketiga yang hanya memberikan file header dan file objek shared library yang ditulis dengan bahasa C. Sedangkan program yang kita tulis menggunakan bahasa C++ yang akan memanggil beberapa fungsi di librari ini. 
     Pada proses kompilasi, simbol fungsi di file header akan dilakukan proses mangling, sehingga pada proses linking akan mengakibatkan kesalahan, karena simbol tidak dapat ditemukan. Untuk mencegahnya, simbol fungsi pada file header tidak boleh dilakukan proses mangling oleh kompiler. Ini dapat dilakukan dengan menambahkan deklarasi extern "C" di file header yang akan membuat kompiler untuk memakai konvensi nama C (C name convention). Karena pada kompiler C++ biasanya didefinisikan makro __cplusplus maka penambahan header dapat dilakukan sbb: 

#ifdef __cpluplus
extern "C" {
#endif

......

#ifdef __cpluplus
}
#endif
     Jika librari dimuatkan menggunakan Dynamic Loading dan fungsi di librari dipanggil dengan memakai fungsi dlsym(), maka masalah di atas tidak akan terjadi, karena pada simbol fungsi di librari tidak dilakukan proses mangling. Masalah lain yang timbul sehubungan dengan pemakaian bahasa C++ adalah kita tidak dapat memakai fungsi dlsym() untuk memanggil fungsi di librari yang ditulis memakai bahasa C++. Hal ini diakibatkan karena pada semua simbol akan dilakukan proses mangling, sehingga kita tidak akan tahu nama simbol dari fungsi yang kita inginkan. 
7.Penutup 
     Penerapan teknik Dynamic Loading akan memudahkan pengembangan suatu aplikasi besar yang dilakukan oleh sekelompok pemrogram seperti halnya aplikasi web server Apache. Program utamanya hanya mengimplementasi fungsi pokok saja, sedang fungsi-fungsi lainnya ditambahkan dalam bentuk modul secara fleksibel bergantung dari keperluan. Salah satu faktor yang tidak menguntungkan adalah lamanya waktu proses pemuatan file shared library ke memori dan proses pereferensian dari simbol. Jika modul-modul dimuatkan hanya pada saat awal saja, maka faktor di atas tidak terlalu penting. Aplikasi bot irc eggdrop memakai teknik lain untuk menghindari faktor di atas. Pengaksesan ke simbol dilakukan dengan melalui tabel yang berisikan pointer statis dari simbol. Akhir kata, semoga artikel ini bermanfaat dan dapat menambah wacana teknik pemrograman bagi pembaca. 

Eko Bono Suprijadi 

| Atas  |

Email : jakarta@jakarta.linux.or.id


| Home | Halaman Muka
 © 1999-2002  ELEKTRO Online
All Rights Reserved.
Last modified on :