Xử lý ngoại lệ trên Symbian (Leave-Symbian exeption)

Bài viết sưu tầm từ trang này.

1. Cơ chế bắt lỗi trên Symbian

Nếu bạn đã quen với lập trình C++ hay Java thì exeption handling là một khái niệm chẳng xa lạ gì. Đây là cơ chế giúp ta quản lý các lỗi phát sinh. Lúc Symbian được thiết kế thì cơ chế exeption chưa được giới thiệu trong C++ hơn nữa sau này khi được giới thiệu thì nó cũng tỏ ra không phù hợp trong môi trường hạn chế về xử lý và bộ nhớ như Symbian bởi chúng làm tăng đáng kể kích thước mã biên dịch và tốn nhiều RAM, lại không thật sự hiệu quả.

Vì vậy Symbian đã đưa ra một cơ chế quản lý lỗi cho riêng mình được biết dưới tên gọi “leave”. Do đó, tuy Symbian sử dụng cú pháp C++ nhưng không hề có từ khóa try, catch hay throw đâu, các bạn nên chú ý.

Trong một môi trường mà tài nguyên hạn hẹp như điện thoại Symbian thì một cơ chế bắt lỗi hiệu quả và ít tốn kém sẽ rất cần thiết. “Leave” đã đáp ứng điều này. Cơ chế hoạt động của “leave” như sau: “Khi xảy ra một lỗi nào đó (thiếu bộ nhớ để cấp phát, thiếu vùng nhớ để ghi, lỗi trong truyền thông hay thiếu năng lượng cho các tài nguyên,…) thì hàm đang hoạt động sẽ bị ngắt lại, quyền điều khiển sẽ được chuyển đến phần chỉ thị sửa lỗi”.

Xét về mặt cú pháp thì cơ chế “leave” này khá tương đồng với cơ chế của C++. Hàm đang thực thi bị ngắt bởi một lệnh gọi đến hàm User::Leave() hay User::LeaveIfError() khá giống với throw trong C++ còn 2 marco TRAP và TRAPD trên Symbian thì tương đồng với try và catch trên C++.

Ví dụ:

TInt result;
TRAP(result, MyLeaveL());
if (KErrNone == result)
{
    //Code
}
User::LeaveIfError(result);

2. Hàm leave

Như tôi đã nói trong phần quy ước trên Symbian, hàm có thể leave thì sẽ kết thúc bằng chữ L. Một hàm có thể leave nếu nó:

  • Gọi hàm có thể leave mà không được gọi kèm với các trap harness như TRAP hay TRAPD (TRAPDD chỉ khác TRAP ở chỗ nó tự động khai báo biến result luôn).
  • Gọi một trong các hàm hệ thống đảm nhận ném ra leave như User::Leave() hay User::LeaveIfError(),…
  • Có dùng toán tử new (Eleave) ví dụ: CExam* e = new (ELeave) CExam().

Chắc có lẽ có nhiều bạn sẽ thắc mắc tại sao tôi lại quá chú trọng đến “leave” như vậy. Thật ra “leave” là một khái niệm rất cơ bản trên Symbian bởi vì:

  • Thứ nhất, nguồn tài nguyên trên Symbian khá hạn hẹp nên lỗi thiếu tài nguyên hay xảy ra.
  • Thứ 2, nếu bạn không chú ý kỹ đến nó, nhất là phần thế nào là một hàm leave thì bạn sẽ gặp phải lỗi rất lớn trong lập trình trên Symbian: gây ra “leak” bộ nhớ.

3. An toàn hơn với CleanupStack

Khi hàm của bạn có leave xảy ra thì tại thời điểm leave, điều khiển sẽ được chuyển đến phần xử lý lỗi, lúc này vùng stack cho hàm có leave này sẽ được giải phóng, các biến khai báo cục bộ trong hàm này sẽ bị xóa đi. Đối với các biến khai báo kiểu T trên stack thì không sao nhưng đối với các biến kiểu C khai báo trên heap hay các biến kiểu R thì đây là vấn đề nghiêm trọng. Bởi lẽ theo đúng quy trình thực thi, nếu không có gì xảy ra thì vào cuối hàm, chúng ta sẽ hủy vùng nhớ đối tượng trên heap qua toán tử delete hay gọi hàm Close() cho các biến kiểu R nhưng nếu giữa chừng hàm bị ngắt trước khi ta gọi các hàm hủy này thì rõ ràng các đối tượng này sẽ không được giải phóng hoàn toàn, tạo ra leak (lỗ hổng) trên bộ nhớ. (Bạn có nhớ finally trong Java ko?)

Leak bộ nhớ là vùng nhớ trên heap thực sự không được sử dụng nhưng hệ điều hành nghĩ là nó đang sử dụng và sẽ không sử dụng vùng nhớ này để cấp phát cho các đối tượng khác. Leak bộ nhớ thường do bạn cấp phát động bộ nhớ (sử dụng hàm new, …) mà không giải phóng nó (hàm delete,…). Bị leak bộ nhớ sẽ gây ra sự lãng phí tài nguyên bộ nhớ, đặc biệt trên thiết bị giới hạn về tài nguyên như di động thì là cả một vấn đề.

Ví dụ:

void UnsafeFunctionL()
{
    CExClass* test = CExClass::NewL(); //Hàm có thể leave
    FunctionMayLeaveL();
    delete test;
}

Điều gì xảy ra khi hàm FunctionMayLeaveL() bị leave, lúc này hàm UnsafeFunctionL() sẽ bị ngắt, stack sẽ bị xóa, biến test bị bị xóa nhưng vùng nhớ cấp cho nó trên heap qua hàm CExClass::NewL() thì vẫn còn và lúc này không ai quản lý nó cả, nó bị “mồ côi” trên heap. Vùng nhớ cấp phát này sẽ tồn tại mà không được giải phóng tại ra một lỗ hổng trong bộ nhớ.

Vậy bây giờ ta phải làm sao đây để luôn đảm bảo không bị lỗ hổng trên bộ nhớ khi có leave xảy ra? Symbian đã đưa ra khái niệm mới là Cleanup Stack. Cleanup stack là một ngăn xếp có nhiệm vụ giải phóng các vùng nhớ cấp cho các đối tượng được đưa vào chúng trước đó khi leave xảy ra.

Ví dụ: Dùng hàm trên với cleanup stack:

void SafeFunctionL()
{
    CExClass* test = CExClass::NewL(); //Hàm có thể leave
    CleanupStack::PushL(test);
    FunctionMayLeaveL();
    CleanupStack::Pop();
    delete test;
}

Lúc này nếu có leave xảy ra thì chúng ta vẫn không sợ bị lủng bộ nhớ vì cleanup stack đã giải phóng vùng nhớ cho biến test giùm chúng ta rồi nhờ hàm: CleanupStack::PushL().

Một số lưu ý:

  • Nếu leave không xảy ra thì chúng ta phải lấy đối tượng cần hủy ra khỏi cleanup stack qua hàm CleanupStack::Pop() (chúng ta có thể dùng nhiều cách Pop khác nhau, chi tiết các bạn xem qua lớp CleanupStack).
  • Với những hàm kế thúc bằng LC, nghĩa là có thể leave và đã có push lên cleanup stack rồi, nên sau khi gọi hàm này, bạn phải gọi hàm CleanupStack::Pop() hoặc CleanupStack::PopAndDestroy() nếu không sẽ bị lỗi. Lỗi này tôi cũng đã nói trong phần quy ước rồi, các bạn chú ý nhé, hay bị lỗi này lắm đó.
  • Đối với các đối tượng kế thừa các lớp khác ngoài lớp C thì khi hủy cleanup stack chỉ có thể hủy vùng nhớ mà không thể gọi destructor như đối với lớp C được nên Symbian đề xuất một số hàm push khác cho phù hợp: CleanupReleasePushL() để chỉ giải phóng vùng nhớ (đối tượng lớp T), CleanupDeletePushL() để chỉ thực thi destructor (đối tượng lớp M) hay CleanupClosrPushL() để giải phóng tài nguyên cấp cho các đối tượng lớp R.

Leave và Cleanup stack là một cặp bài trùng tạo nên sự an toàn cho lập trình trên Symbian. Đây là 2 khái niệm rất cơ bản, nếu không hiểu về nó, trong khi lập trình có thể bạn sẽ gặp lỗi mà không biết đường sửa hay có thể gặp vài lỗi rất ngớ ngẩn.

4. Khởi tạo hai pha (Two-phase construction) trong Symbian

Đến đây chắc bạn đã thấy là Symbian hỗ trợ quản lý bộ nhớ tốt như thế nào, đảm bảo cả trong điều kiện lỗi vẫn không bị lủng bộ nhớ. Lý do khiến Symbian  rất chú trọng đến việc này là so với PC, điện thoại di động có bộ nhớ không lớn bằng hơn nữa các ứng dụng trên điện thoại đôi khi phải chạy hàng tháng, thậm chí hàng năm (nếu ta không tắt máy).

Từ bài cleanup stack, có người thắc mắc là có phải tất cả các lớp đều có hàm NewL() và NewLC() để khởi tạo đối tượng không. Nhận thấy có một phần quan trọng đi liền sau leave và cleanup stack là khởi tạo 2 pha (two-phase construction) nên tôi sẽ xin nói về nó luôn.

a. Khởi tạo hai pha (Two-phase construction)

Theo đúng cú pháp C++, một đối tượng mới sẽ được cài đặt như sau:

CExam* exam = new (Eleave) CExam();

Hệ thống sẽ làm gì với code trên: “Đầu tiên, một vùng nhớ sẽ được cấp trên heap cho đối tượng lớp CExam nhớ hàm new, rồi sau đó constructor của lớp CExam sẽ được gọi để hoàn tất việc khởi tạo đối tượng. Điều gì sẽ xảy ra nếu hàm constructor của lớp này bị leave, rõ ràng là bộ nhớ sẽ bị lủng do heap đã cấp một vùng cho đối tượng foo rồi và như vậy vùng nhớ này sẽ bị “mồ côi” trên heap”.

=> Lúc này ắt hẳn bạn sẽ nghĩ ngay đến cleanup stack, thế nhưng tiếc thay trong trường hợp này lại không dùng được. Tai sao ư? Tại vì để làm được điều đó thì hàm CleanupStack::PushL() phải đặt trong lòng toán tử new, điều này có thể sao?!

Vì vậy Symbian đưa ra một luật là: “constructor không được phép leave”. Nhưng đôi khi trong phần khởi tạo của chúng ta lại có phần có thể leave thì sao, chẳng hạn như cấp phát bộ nhớ hay tạo một session truy cập tài nguyên. Trong tình huống đó, khởi tạo 2 pha (two-phase construction) sẽ giúp bạn:

  • Pha 1: Phần constructor đơn giản, không leave. Phần này sẽ được gọi liền ngay sau khi toán tử new được gọi.
  • Pha 2: Một hàm khác sẽ đảm nhận việc hoàn tất khởi tạo, trên Symbian thường đặt tên là ConstructL(), hàm này có thể leave.
CExam* exam = new (Eleave) CExam(); // Pha 1
CleanupStack::PushL(exam);
ContructL(); // Pha 2
CleanupStack::Pop();

Và bây giờ thì ta đã yên tâm là bất cứ có gì xảy ra, bộ nhớ vẫn nguyên vẹn.

b. NewL() và NewLC()

Nhưng cách trên bất tiện ở chỗ là khi khởi tạo 1 đối tượng lại phải gọi 2 pha với 4 hàm, vừa bất tiện vừa dễ quên. Do đó Symbian tiếp tục đưa ra một khái niệm nữa để giúp cho lập trình viên chúng ta tránh được sự hay quên và dài dòng này. Trong lớp CExam, chúng ta tạo thêm 2 hàm tĩnh (có từ khóa static đằng trước phần khai báo thông thường) NewL() và NewLC() như sau:

CExam* CExam::NewLC()
{
    CExam* self = new (Eleave) CExam(); // Pha 1
    CleanupStack::PushL(self);
    ContructL(); //Pha 2
    return self;
}

CExam* CExam::NewL()
{
    CExam* self = CExam::NewLC();
    CleanupStack::Pop(); // đến đây là khởi tạo đã thành công
    return self;
}

Và lúc này việc khai báo đối tượng của bạn sẽ vừa dễ dàng lại đảm bảo:

CExam* exam = CExam::NewL();

Lưu ý: Ở trên các bạn thấy tôi luôn xài Eleave sau toán tử new. Nhờ nó mà nếu không cấp phát được, leave sẽ xảy ra. Nếu không có nó rõ ràng sau hàm new bạn phải kiểm tra xem có cấp phát thành công không, rất mất công lại làm code phức tạp thêm.

CExam* exam = new CExam();
if (NULL != exam)
{
// Cấp phát thành công
}

Tóm lại: Nếu trong hàm constructor mà không có gì để gây ra leave cả, thì các bạn cứ xài như đã từng xài trước đây, nghĩa là dùng code như C++ vậy. Nếu trong hàm constructor này mà có leave thì bạn mới phải cần đến two-phase construction.

Advertisements

One thought on “Xử lý ngoại lệ trên Symbian (Leave-Symbian exeption)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s