কম্পিউটার টিউটোরিয়াল

C Extension-এ Ruby মেথড আহ্বান করলে মেমরি লিক প্রতিরোধ করুন

মেমরি লিক gem ইউজারদের জন্য বেদনা। এগুলো ট্র্যাক করা কঠিন এবংvaluable infrastructure cost-এ dẫn করতে পারে।

C extension-এ মেমরি লিক আরও খারাপ। Ruby-তে লিক খুঁজার অনেক টুল ও আর্টিকেল আছে। কিন্তু C-এ ইন্টারনাল অ্যাক্সেস সেম নয়।

rb_funcall নেইভ ব্যবহার মেমরি লিক করে দিতে পারে: rb_protect ব্যবহার করা ভালো। তাই যদি আপনি C extension writer হন, তবে আপনার gem ব্যবহারকারী ডেভেলপারদের জন্য পড়ুন।

rb_funcall এবং C-র সমস্যা

rb_funcall একটি গ্রেট টুল যখন Ruby এবং C পার্টের মাঝপথে ইন্টারঅ্যাক্ট করতে হয় কিন্তু কম C লিখতে হয়।

তবে rb_funcall রান করলে আপনি আর C-তে নই যেখানে সব কিছু straightforward। আপনি muddy waters-এ পড়ে যেতে পারেন যদি কল করা ফাংশন:

  1. রানটাইমে পুরোপুরি তার ডেফিনিশন চেঞ্জ করে
  2. একটি কল রেইজ করে

নম্বর ১ সহজে ক্যাচ হয় — সেগফল্ট হবে, এবং যদি টেস্ট সিউইট কমপ্লিট হয় তবে পাবলিশ করার আগে ক্যাচ হবে।

“但是"، দ্বিতীয়টি মেমরি লিক করতে পারে এবং কোডবেস কঠিন করে তুলতে পারে পড়তে। এখন সেটা দেখি।

Ruby Raise করলে C Memory Leak

Ruby-এর raising mechanism কোডের এক স্কোপ থেকে প্রথম প্যারেন্ট স্কোপে জাম্প করে যা এরর ক্যাচ করে। MRI-তে এটি longjmp এবং setjmp দিয়ে ইমপ্লিমেন্ট করা।

যদি আপনি begin..ensure ব্লক ব্যবহার করেন, আপনি setjmp() করছেন, এবং এই ব্লকের ভিতরে raise করলে longjmp() দিয়ে সেভড পজিশনে ফিরে যান।

সেজন্য যদি কোনো ফাংশন rb_funcall-এ রেইজ হয়, তবে সেটার পরে C কোড কখনো এক্সিকিউট হবে না।

নিচের উদাহরণে পোটেনশিয়াল লিক দেখানো হয়েছে। যদি json_parse রেইজ করে, তবে লিক হবে:

VALUE parse_json(VALUE self, VALUE str) {
  char *c_str = StringValueCStr(str);
  json_t *json = json_parse(c_str); // যদি এটি raise করে
  free(c_str); // এই লাইন এক্সিকিউট হবে না!
  return json_to_ruby(json);
}

উপরের উদাহরণ একটু silly — আপনি freeing এবং Ruby processing পার্ট ইনভার্ট করতে পারেন। কিন্তু সবসময় সম্ভব নয়, এবং লম্বা ফাংশন বডি বেশি intertwined হতে পারে।

Ruby-এ begin..ensure ব্যবহার

Ruby-তে আপনি উপরের উদাহরণ begin..ensure দিয়ে লিখতে পারেন:

def parse_json(str)
  c_str = str.to_s
  json = JSON.parse(c_str)
ensure
  c_str.free # Ruby-এ এটাও অটোমেটিক হয় GC-র মাধ্যমে
end

এই API C-তেও rb_rescue এবং rb_ensure দিয়ে উপলব্ধ:

VALUE parse_json_protected(VALUE self, VALUE str) {
  char *c_str = StringValueCStr(str);
  json_t *json = json_parse(c_str);
  rb_ensure(free_c_str, (VALUE)c_str, 0, 0);
  return json_to_ruby(json);
}

কিন্তু এটি একটু cumbersome, এবং rescue ব্লক যোগ করলে readability আরও কমে যায়। Peter Zhu-এর 'A Rubyist's Walk Along the C-side (Part 8): Exceptions & Error Handling' পড়ুন যদি C-তে begin..rescue..ensure..end API ব্যবহার করতে চান।

C-তে rb_protect ব্যবহার

আরেকটি অপশন। প্রথমে Ruby-তে কীভাবে দেখতো:

begin
  json = JSON.parse(str)
rescue => e
  # handle error
  raise e
end

এটি Ruby-তে অদ্ভুত লাগে, কিন্তু C-তে খুব উপযোগী। MRI-তে এর জন্য API আছে: rb_protect। C ফাংশন এমন হবে:

VALUE parse_json_protected(VALUE self, VALUE str) {
  int state;
  VALUE result = rb_protect(parse_json_internal, str, &state);
  if (state) {
    // error occurred, but we can cleanup first
    free_c_str(str);
    rb_jump_tag(state); // re-raise the error
  }
  return result;
}

VALUE parse_json_internal(VALUE str) {
  char *c_str = StringValueCStr(str);
  json_t *json = json_parse(c_str);
  free(c_str);
  return json_to_ruby(json);
}

উপরের মেথড সব কিছু ফ্রি করার পর Ruby এরর re-raise করবে।

নোট: আমরা এরর ইগনোর করতে পারি খালি rescue ব্লক দিয়ে Ruby-তে:

begin
  json = JSON.parse(str)
rescue
  # ignore
end

Warning: যদি এরর re-raise না করেন, rb_set_errinfo(Qnil) স্টেপ গুরুত্বপূর্ণ যাতে ইউজাররা যেসব এরর জানা উচিত নয় সেগুলোর ইনফো রাখা না যায়।

অথবা কন্ডিশনালি এরর রেইজ করা যাক rescue My::Error:

int state;
VALUE result = rb_protect(parse_json_internal, str, &state);
if (state) {
  VALUE err = rb_errinfo();
  if (rb_obj_is_kind_of(err, MyErrorClass)) {
    rb_jump_tag(state); // re-raise
  } else {
    rb_set_errinfo(Qnil); // clear error info
    free_c_str(str);
  }
}
return result;

rb_errinfo()-কে $! গ্লোবাল ভেরিয়েবলের সমান ভাবতে পারেন।

এটি সব চমৎকার, কিন্তু একটু rb_funcall থাকলে API সিম্পলিফাই করা যায়।

rb_protect API ব্যবহারের মূল আইডিয়া: readability বাড়ানো। আপনাকে চেক করতে হয় না ফাংশনটি raise করতে পারে কিনা, আপনি অনুমান করুন সেটা পারে, এবং state দিয়ে কাজ করুন।

rb_protect_funcall প্রপোজাল

rb_funcall আলাদা করি, কারণ এটি একমাত্র dangerous মেথড। এখানে একটি API যা সেটা করবে:

VALUE rb_protect_funcall(VALUE recv, ID mid, int argc, const VALUE *argv, int *state) {
  return rb_protect((VALUE (*)(VALUE))rb_funcallv, (VALUE)argv, state);
}

এই API rb_funcall-এর সমান, শুধু rb_protect-এর state যুক্ত। ব্যবহার খুব সিম্পল:

int state;
VALUE result = rb_protect_funcall(self, id_parse, 1, &str, &state);
if (state) {
  // handle error
}

এই API এখনো Ruby-তে নেই, এবং কখনোই নাও হতে পারে। RGeo (MIT LICENSE) থেকে নেওয়া যাবে।

Real-World Example

রিয়েল-ওয়ার্ল্ড উদাহরণ দেখতে চাইলে RGeo কোডবেস পড়ুন — আমরা gần সম্পূর্ণ rb_protect-এ স্যুইচ করেছি। rgeo_convert_to_geos_geometry ফাংশনটি এই state propagate করে সিম্পলার ইউজের জন্য। সেটা থেকে ডিগিং শুরু করুন।

RGeo-এ issue খুলুন আরও ডিসকাশনের জন্য।

উপসংহার

এই পোস্টে আমরা সতর্ক করেছি rb_funcall C-এ ব্যবহার করার বিরুদ্ধে কারণ এটি মেমরি লিক করতে পারে। begin..ensure বা rb_protect ব্যবহার করার বিকল্প এক্সপ্লোর করেছি।

Happy coding!

P.S. Ruby Magic পোস্টগুলি প্রকাশ होते সাথে সাথে পেতে আমাদের Ruby Magic নিউজলেটার সাবস্ক্রাইব করুন!

C Extension-এ Ruby মেথড আহ্বান করলে মেমরি লিক প্রতিরোধ করুন

Ulysse Buonomo

আমাদের অতিথি লেখক Ulysse একজন ফর্মার ইন্ডাস্ট্রি Ruby ডেভেলপার যারা অধিকাংশ সময় বিশ্ব ভ্রমণে ব্যতিত করেন। স্পেয়ার টাইম RGeo এবং Ruby-এ, এবং Ruby-এর ইন্টারনালস সাথে টিংকার করা পছন্দ।

Ulysse Buonomo-এর সব আর্টিকেল

  1. C++-এ জব শিডিউলিং থেকে সর্বোচ্চ প্রফিট (Maximum Profit in Job Scheduling)

  2. Stoffle প্রোগ্রামিং ল্যাঙ্গুয়েজের জন্য রুবি-ভিত্তিক পার্সার তৈরি

  3. অ্যান্ড্রয়েড ডিভাইসের র‍্যাম কম নাকি বেশি তা কিভাবে জানবেন?

  4. জাভাস্ক্রিপ্টে সিলেক্ট লিস্টের ভ্যালু অ্যারেতে আছে কিনা চেক করে কনসোলে দেখানো