Reference Counting: Cara Cocoa, WinRT, dan COM Mengelola Memori Objek
Artikel ini membahas tentang teknik reference counting yang digunakan dalam tiga framework berbeda: Cocoa untuk pengembangan aplikasi di platform Apple, WinRT untuk pengembangan aplikasi di platform Windows, dan Component Object Model (COM) yang digunakan dalam sistem operasi Windows. Kami akan membahas bagaimana ketiga framework ini menggunakan reference counting untuk mengelola memori objek dan membantu menghindari kebocoran memori.
Ada beberapa cara sebuah program mengelola memori dan siklus hidup sebuh objek
dalam suatu program. Java dan Go memilih menggunakan Garbage Collector yang
merupakan mekanisme otomatis untuk mengelola memori dan menghapus objek yang
tidak lagi digunakan secara otomatis. Sementara itu, bahasa pemrograman seperti
C dan C++ menggunakan teknik manual memory management dengan penggunaan fungsi
alokasi seperti malloc
dan free
atau kata kunci seperti new
dan delete
untuk mengelola alokasi dan dealokasi memori.Bahasa seperti Rust bahkan memilih
menggunakan compiler untuk melacak lifetime dari suatu variabel.
Selain cara-cara di atas ada satu cara lain yang memilih mengelola memori objek dengan reference counting. Reference Counting adalah suatu cara untuk mengelola siklus hidup objek dengan cara menghitung berapa kali sebuah objek direferensikan oleh objek lain. Ketika sebuah objek dibuat, referensi countnya bernilai satu. Setiap kali sebuah objek lain mereferensikan objek tersebut, reference count akan bertambah satu. Sebaliknya, ketika sebuah objek tidak lagi mereferensikan objek tersebut, reference count akan berkurang satu. Ketika reference count bernilai nol, objek akan dihapus dari memori.
Contoh Implementasi Reference Counting dalam C
Reference counting bisa diekspresikan dengan pseudo-code seperti di bawah ini.
struct person_rc_t {
int age;
int id;
char name[256];
// hitungan referensi
int ref;
};
// konstruksi objek
person_rc_t *create_new_person(int age, int id, char *name, size_t charlen) {
size_t sz = charnel > 255 ? 255 : charlen;
person_rc_t *p = malloc(sizeof(struct person_rc_t));
p->age = age;
p->id = id;
memcpy(p->name, name, sz);
p->name[sz] = '\0';
p->ref = 1;
return p;
}
// mengakuisisi objek
person_rc_t *acquire_person(struct person_rc_t *p) {
++ p->ref;
return p;
}
// melepas objek
void release_person(struct person_rc_t *p) {
-- p->ref;
if (p == 0) {
free(p);
}
}
void use_person(void) {
person_rc_t *person = create_new_person(34, 4, "Eko", 3);
person_rc_t *another = acquire_person(person);
/* lakukan sesuatu dengan another dan person */
release_person(another); // ref == 1
release_person(person); // ref == 0
}
Di dalam tipe person_rc_t
tersebut terdapat variabel untuk menghitung
referensi, di dalam kasus kita di atas, referensi disimpan dalam variabel ref
di dalam tipe person_rc_t
. Untuk 'mendapatkan' referensi ke pointer person
pemanggil harus menggunakan fungsi acquire_person
yang akan menambahkan satu
referensi. Dan jika sudah selesai memakai, maka kita memakai release_person
yang akan mengurangi referensi. Ketika referensi 0, fungsi free
akan
dipanggil.
Sebenarnya teknik reference counting ini juga dipakai di Garbage Collector1. Perbedaannya, ada di algoritmanya. Di sini kita merilis manual, sementara di GC, algoritma yang dipakai adalah algoritma yang otomatis akan membersihkan objek seperti Concurrent Mark and Sweep2.
Penggunaan Reference Counting dalam COM
COM (Component Object Model) adalah teknologi yang dibuat oleh Microsoft untuk membuat aplikasi bisa berkomunikasi satu sama lain. Nama-nama lain yang sering dipakai oleh Microsoft adalah OLE, OLE2, OLE Automation. COM dibuat dengan ABI yang kompatibel dengan ABI dari Microsoft C++ dan ditujukan supaya bisa dikonsumsi oleh bahasa apapun yang mendukung COM seperti Visual Basic dan C#.
Komponen yang dibuat dalam COM berorientasi objek dan dikelola dengan menggunakan reference counting. Superclass dari objek COM adalah interface yang didefinisikan kurang lebih seperti ini.
#include <unknwn.h>
class IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE QueryInterface(
/* [in] */ REFIID riid,
/* [iid_is][out] */ _COM_Outptr_ void __RPC_FAR* __RPC_FAR* ppvObject) = 0;
virtual ULONG STDMETHODCALLTYPE AddRef( void) = 0;
virtual ULONG STDMETHODCALLTYPE Release( void) = 0;
};
Abaikan anotasi-anotasi seperti _COM_Outptr_
dan RPC_FAR
. Saya akan
menyoroti dua fungsi paling bawah yaitu AddRef
dan Release
. Kedua fungsi
tersebut adalah fungsi untuk melakukan reference counting. Sementara
QueryInterface
dipergunakan untuk mengkonversi dari satu antarmuka ke
antarmuka yang lain.
Contoh penggunaan COM bisa dilihat di kode di bawah ini yang membuat pintasan(shortcut) di destop.
#include <Windows.h>
#include <ShlObj.h>
#include <string>
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
// Inisialisasi COM
CoInitialize(NULL);
// Buat instance dari objek IShellLink
IShellLink *pShellLink = nullptr;
HRESULT hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (void **)&pShellLink);
if (SUCCEEDED(hr))
{
// Atur properti pintasan
pShellLink->SetPath("C:\\Windows\\System32\\notepad.exe");
pShellLink->SetDescription("Pintasan ke Notepad");
// Ambil antarmuka IPersistFile
IPersistFile *pPersistFile = nullptr;
hr = pShellLink->QueryInterface(IID_IPersistFile, (void **)&pPersistFile);
if (SUCCEEDED(hr))
{
// Simpan pintasan di desktop
WCHAR desktopPath[MAX_PATH];
SHGetSpecialFolderPath(NULL, desktopPath, CSIDL_DESKTOP, FALSE);
std::wstring shortcutPath = std::wstring(desktopPath) + L"\\Pintasan Notepad.lnk";
hr = pPersistFile->Save(shortcutPath.c_str(), TRUE);
// Lepaskan antarmuka IPersistFile
pPersistFile->Release();
if (SUCCEEDED(hr))
{
MessageBox(NULL, L"Pintasan berhasil dibuat", L"Informasi", MB_OK | MB_ICONINFORMATION);
}
else
{
MessageBox(NULL, L"Gagal menyimpan pintasan", L"Kesalahan", MB_OK | MB_ICONERROR);
}
}
else
{
MessageBox(NULL, L"Gagal mengambil antarmuka IPersistFile", L"Kesalahan", MB_OK | MB_ICONERROR);
}
// Lepaskan antarmuka IShellLink
pShellLink->Release();
}
else
{
MessageBox(NULL, L"Gagal membuat objek IShellLink", L"Kesalahan", MB_OK | MB_ICONERROR);
}
// Hentikan inisialisasi COM
CoUninitialize();
return 0;
}
Ketika antarmuka IShellLink
dikonversi menjadi IPersistFile
dengan
QueryInterface
, referensi dari objek yang dibuat dengan CoCreateInstance
bertambah satu. Dan ketika kita selesai memakai objek IShellLink, kita lepas
dengan Release
. Di baris Release
terakhir, objek akan dibebaskan dari
memori.
API di Windows modern yang dibuat oleh Microsoft sebagian besar adalah COM API. Bahkan platform baru, yaitu UWP (Universal Windows Platform) meninggalkan paradigma Win32 API yang berbasis C dan antarmuka pemrogramannya adalah iterasi dari COM.
Komponen-komponen COM Moderen biasanya dibuat menggunakan C++. Dan Microsoft berinvestasi cukup besar dalam bahasa C++. Untuk WinRT, Microsoft bahkan membuat projection dari C++20 yang dinamai C++/WinRT.
Video di bawah ini menerangkan lebih lanjut soal keputusan Microsoft untuk memakai C++20 dan C++/WinRT. Video dalam Bahasa Inggris.
Penggunaan Reference Counting di platform Apple
Apple, menggunakan paradigma ini dari platform yang dibuat oleh NextSTEP. Di iklannya, NextSTEP dipromosikan sebagai "platform berorientasi objek". Maksud dari slogan kurang lebih adalah semua hal adalah objek dan dikelola dengan reference counting. Mari kita lihat program sederhana di zaman NextStep.
#include <appkit/Application.h>
#include <appkit/Window.h>
@interface AppDelegate : Object
{
Window *window; // deklarasi instance variable untuk window
}
- (void)applicationDidFinishLaunching: (NSNotification *)notification; // method untuk mengatur window ketika aplikasi selesai diluncurkan
- (void)applicationWillTerminate: (NSNotification *)notification; // method untuk membersihkan objek window ketika aplikasi akan ditutup
@end
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
window = [[Window alloc] initWithContentRect:MakeRect(0, 0, 300, 200)
style: (NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask)
backing: NXBuffered
defer: NO];
[window setTitle: @"NextSTEP Window"]; // atur judul window
[window center]; // letakkan window di tengah layar
[window makeKeyAndOrderFront: nil]; // tampilkan window di depan
}
- (void)applicationWillTerminate:(NSNotification *)notification {
[window release]; // bebaskan objek window sebelum aplikasi ditutup
}
@end
int main(int argc, const char *argv[]) {
id application = [Application sharedApplication]; // buat objek aplikasi
id appDelegate = [[AppDelegate alloc] init]; // buat objek delegate
[application setDelegate: appDelegate]; // atur delegate aplikasi
[application run]; // jalankan aplikasi
[appDelegate release]; // bebaskan objek delegate
return 0;
}
Di kode objective-C di atas, kita membuat objek delegate
untuk kemudian di set
ke objek application
. Ketika apilkasi mau diterminasi, objek window
dilepaskan dengan memanggil release
. Tidak ada metode semacam QueryInterface
seperti di Windows, karena objective-c adalah bahasa dengan tipe lemah.
Automatic Reference Counting
Di macOS moderen, keperluan reference counting ditangani oleh fitur compiler yang dinamai Automatic Reference Counting (ARC). ARC ini akan mendeteksi kapan harus di-retain dan release. Ini mengatasi permasalahan yang dinamakan dengan retain cycle yaitu keadaan dimana dua objek saling me-retain satu sama lain sehingga tidak bisa dilepaskan.
Di macOS moderen, kode NextSTEP di atas menjadi seperti di bawah ini:
#import <Cocoa/Cocoa.h>
@interface AppDelegate : NSObject <NSApplicationDelegate>
@property (nonatomic, strong) NSWindow *window; // deklarasi instance variable window dengan property
@end
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
self.window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 300, 200)
styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable // atur gaya window dengan NSWindowStyleMask
backing:NSBackingStoreBuffered
defer:NO];
[self.window setTitle:@"NextSTEP Window"]; // atur judul window
[self.window center]; // letakkan window di tengah layar
[self.window makeKeyAndOrderFront:nil]; // tampilkan window di depan
}
- (void)applicationWillTerminate:(NSNotification *)notification {
self.window = nil; // bebaskan objek window ketika aplikasi akan ditutup
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSApplication *application = [NSApplication sharedApplication]; // buat objek aplikasi
AppDelegate *appDelegate = [[AppDelegate alloc] init]; // buat objek delegate
[application setDelegate:appDelegate]; // atur delegate aplikasi
[application run]; // jalankan aplikasi
}
return 0;
}
Ada anotasi baru seperti (nonatomic, strong)
yang menandakan bahwa objek
window dipunyai (retained) oleh AppDelegate
dan siklus hidupnya selaras
dengan siklus AppDelegate
. Di kode di atas, walaupun tetap ada reference
counting, penulisan retain
dan release
akan dikerjakan secara implisit oleh
compiler. Kode menjadi lebih ringkas dan lebih mudah dibaca. Bahkan, di bahasa
terbaru yang dibuat oleh Apple, Swift. Kode di atas jadi lebih singkat lagi.
import Cocoa
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ notification: Notification) {
window = NSWindow(contentRect: NSMakeRect(0, 0, 300, 200),
styleMask: [.titled, .closable, .resizable],
backing: .buffered,
defer: false)
window.title = "NextSTEP Window"
window.center()
window.makeKeyAndOrderFront(nil)
}
func applicationWillTerminate(_ notification: Notification) {
window = nil
}
}
Mungkin karena inilah, Apple pernah bikin promosi di mana, walaupun pakai prinsip yang sama, yaitu reference counting. Memrogram di Apple terasa lebih mudah karena sintaks Objective-C dan Swift lebih mudah dipahami daripada C++.
Kesimpulan
Dalam artikel ini, kita telah membahas tentang teknik reference counting dan bagaimana teknik ini digunakan untuk mengelola memori objek dalam beberapa framework seperti Cocoa, WinRT, dan COM. Dalam reference counting, objek akan dihapus dari memori ketika referensi objek tersebut bernilai nol.
Kita juga melihat contoh implementasi reference counting pada bahasa pemrograman C dan bagaimana teknik ini digunakan pada teknologi COM dan platform Apple. Pada macOS moderen, keperluan reference counting ditangani oleh fitur compiler yang dinamai Automatic Reference Counting (ARC).
Meskipun teknik reference counting memiliki keuntungan dalam mengelola memori objek, terdapat juga beberapa kelemahan seperti retain cycle yang dapat terjadi pada beberapa kasus. Dalam kasus seperti itu, pengguna harus memastikan untuk membebaskan referensi objek dengan benar untuk mencegah kebocoran memori.
Footnotes
-
Salgado, Pablo Galindo. Garbage Collection Design. Retrieved April 4, 2023. ↩
-
Jones, Richard. 2001. Antony Hosking, and Eliott Moss. The Garbage Collection Handbook. Amazon ↩