Python Virtualenv vs Pyenv

Awalnya bingung kenapa python perlu virtualenv atau official venv di python 3, padahal udah ada pyenv. Ternyata dua-dua nya memiliki fungsi yang berbeda.

pyenv memudahkan untuk bisa gonta-ganti versi python (v2 atau v3) tanpa harus manual update PATH, atau install-uninstall. pyenv ini serupa dengan ruby rbenv, golang gvm, flutter fvm, dan nodejs nvm.

virtualenv memudahkan penempatan library/package per-project. Sehingga tidak menganggu dependency project lain apabila mereka menggunakan package sama tapi beda version. Konsepnya mirip folder node_modules pada nodejs atau penggunaan go mod vendor pada golang.

How to get Foobar2000 component’s version – Part 2

Let’s create our own Host!

Kita akan membuat host baru yang khusus membaca versi dari komponen. Host menggunakan Python 2.7 dengan module ctypes dan harus berjalan di sistem operasi Windows. Cara yang digunakan adalah ABI bukan API karena kita tidak menggunakan C/C++.

Sebagai bahan eksperimen komponen yang digunakan: foo_rgscan.dll.

import ctypes
import os

foobar_path = "C:\\Program Files (x86)\\foobar2000"
os.environ['PATH'] = foobar_path + ';' + os.environ['PATH']

dllname = foobar + "\\components\\foo_rgscan.dll"
component = ctypes.cdll.LoadLibrary(dllname)

Sebelum kita me-load komponen, kita harus daftarkan path dari host asli-nya ke dalam PATH environment variable. Tujuannya adalah agar module dependency dari komponen bisa ditemukan oleh loader.

Untuk LoadLibrary kita menggunakan CDLL, karena function foobar2000_get_interface yang akan diakses menggunakan cdecl calling convention.

What is Calling Convention ?

Ketika kita memanggil fungsi, secara low-level nilai dari argument/parameter dan return-nya disimpan di suatu tempat. Calling convention inilah yang mengatur tempatnya. Sehingga ada kesepahaman antara pemanggil fungsi caller dan fungsi yang dipanggil callee. Ada tiga konvensi yang sering ditemukan yaitu: cdecl, stdcall, dan thiscall. Sayangnya ctypes tidak mendukung thiscall, namun untuk saat ini bukan masalah. Detailnya: X86_calling_conventions

How Python access C/C++ Classes ?

Sebelum kita dapat menggunakan fitur yang ada pada komponen, kita harus memetakan (mapping) class dari C/C++ ke Python. Caranya menggunakan ctypes.Struct.

Pada executable (exe) atau shared library (dll) file, terdapat minimal dua section yaitu code dan data. Semua function dan method akan disimpan pada section code. Untuk static variable dan const akan disimpan di section data. Ada dua tempat lagi yaitu heap untuk obyek yang dicreate menggunakan new dan stack untuk local variable dan parameter.

Ketika dll ini di-load maka semua section akan masuk ke dalam memory sehingga siap untuk dipakai. Agar dapat menggunakan function ataupun variable yang ada pada dll, kita harus mengetahui alamatnya (address). Export function adalah function yang hanya dapat kita akses karena alamatnya diketahui .

Ketika kita mendapatkan pointer yang di-return dari suatu function makan pointer tersebut mengacu kepada suatu obyek yang ada pada memory. Obyek pada memory tersebut memiliki struktur sesuai class-nya. Struktur dapat diketahui melalui SDK.

Pada C/C++ struktur obyek dari suatu class/struct hanya berisi member variable saja (yang non static) dan virtual function. Sedangkan method tidak disimpan pada obyek tersebut sehingga kita tidak dapat memanggil method dari class tersebut karena tidak diketahui alamatnya. Tetapi virtual function dapat kita panggil (call) karena diketahui alamatnya.

Untuk aturan pembuatan strukturnya adalah sebagai berikut:

  1. Struktur class dimulai dengan sebuah pointer ke table virtual function jika ada.
  2. Diikuti dengan member variable yang non-static sesuai urutan-nya.
  3. Jika class punya parent, maka susunan dimulai dari parent ter atas (virtual function pointer – member-member, virtual function pointer – member-member) dengan pola yang sama. Jika multiple parent struktur sesuai urutan.
  4. Table virtual function berisi alamat ke virtual function sesuai urutan, dan hanya yang di-definisikan pada class tersebut.
  5. Jika virtual function di-override maka yang digunakan adalah alamat override-nya.
  6. Aksesibilitas tidak mempengaruhi urutan (public, private, protected). Urutan yang dimaksud adalah urutan pengetikan pada source code.

Detailnya: Reversing Microsoft Visual C++ Part II: Classes, Methods and RTTI

Let’s break out the structure

Kita mulai dari foobar2000_client_impl yang merupakan interface terhadap komponen.

Offset Member Virtual Offset
foobar2000_client
0 foobar2000_client_vfptr get_version 0
get_service_list +4
+4 pservice_factory_base
foobar2000_component_globals
foobar2000_client_impl

Kemudian foobar2000_client_impl::get_service_list akan me-return (head) service_factory_base atau lebih tepatnya service_factory_base_t. Karena service_factory_base_t::g_instance bukan pointer maka harus dijabarkan sesuai class-nya service_impl_single_t yang merupakan turunan dari T (dalam hal ini T yang dipakai turunan dari service_base).

Offset Member Virtual Offset
service_factory_base
0 service_factory_base_vfptr instance_create 0
+4 *__internal__next
+8 &m_guid
service_factory_base_t
service_factory_single_t
+12 g_instance
+12 service_base_vfptr service_release 0
service_add_ref +4
service_query +8
T_service_virtual +12
+16 T_service_member
+16

Jika T adalah componentversion_myimpl maka g_instance adalah:

Offset Member Virtual Offset
+12 g_instance
+12 service_base_vfptr service_release 0
service_add_ref +4
service_query +8
componentversion_vf get_file_name +12
get_component_name +16
get_component_version +20
get_about_message +24
+16 componentversion_member
+16 componentversion_myimpl_vf
+16 componentversion_myimpl_member

Karena service_base dan componentversion dideklarasikan menggunakan __declspec(novtable) maka virtual function table hanya dibuat tergantung child class yang menggunakannya.

Struktur data yang diperlukan untuk get_component_version adalah pfc::string_base yang menjadi parameter. Alamat function (constructor) tidak diketahui, sehingga kita harus membuat obyek pfc::string_base versi kita. Tidak semua implementasi pfc::string_base harus dibuat tetapi disesuaikan dengan kebutuhan. Jika dilihat pada:

void get_component_version(pfc::string_base & out) {
    out = VERSION;
}

Maka kita perlu melihat cara kerja operator= pada class pfc::string_base.

inline const string_base & operator= (const char * src) {
    set_string_(src);
    return *this;
}

void set_string_(const char * str) {
    set_string(str, _strParamLen(str));
}

virtual void set_string(const char * p_string,t_size p_length = ~0) {
    ...
}

Ternyata kita perlu mendefinisikan virtual method set_string. Namun karena ctypes tidak mendukung thiscall sehingga nilai string yang didapat terpaksa disimpan ke global variable di Python bukan pada member obyek pfc::string_base.

Struktur data terakhir adalah GUID yang sebenarnya adalah 16-bytes, cukup sederhana.

There are Python structures

class GUID(ctypes.Structure):
    _fields_ = [("Data1", ctypes.c_uint32),
                ("Data2", ctypes.c_uint16),
                ("Data3", ctypes.c_uint16),
                ("Data4", ctypes.c_uint8 * 8)]

class service_factory(ctypes.Structure):
    pass
service_factory \
    ._fields_ = [("vfptr_base", ctypes.c_void_p),
                ("next", ctypes.POINTER(service_factory)),
                ("guid", ctypes.POINTER(GUID)),
                ("vfptr_service", ctypes.c_void_p)]

class foobar_client_vfptr(ctypes.Structure):
    _fields_ = [("get_version", ctypes.CFUNCTYPE(ctypes.c_int)),
                ("get_service_list", ctypes.CFUNCTYPE(ctypes.POINTER(service_factory)))]

class foobar_client(ctypes.Structure):
    _fields_ = [("vfptr", ctypes.POINTER(foobar_client_vfptr))]

g_strings = []
pfc_set_string_t = ctypes.WINFUNCTYPE(None, ctypes.c_char_p, ctypes.c_uint)
def pfc_set_string(p_string, p_length):
    g_strings.append( p_string[:p_length] )

class pfc_string_vfptr(ctypes.Structure):
    _fields_ = [("add_string", ctypes.c_void_p),
                ("get_ptr", ctypes.c_void_p),
                ("set_string", pfc_set_string_t)]

class pfc_string(ctypes.Structure):
    _fields_ = [("vfptr", ctypes.POINTER(pfc_string_vfptr))]
    def __init__(self):
        self._vfptr = pfc_string_vfptr()
        self.vfptr = ctypes.pointer(self._vfptr)

        self.vfptr.contents.set_string = pfc_set_string_t(pfc_set_string)

class componentversion_vfptr(ctypes.Structure):
    _fields_ = [("service_release", ctypes.CFUNCTYPE(ctypes.c_int)),
                ("service_add_ref", ctypes.CFUNCTYPE(ctypes.c_int)),
                ("service_query", ctypes.CFUNCTYPE(ctypes.c_bool)),

                ("get_file_name", ctypes.WINFUNCTYPE(None, ctypes.POINTER(pfc_string))),
                ("get_component_name", ctypes.WINFUNCTYPE(None, ctypes.POINTER(pfc_string))),
                ("get_component_version", ctypes.WINFUNCTYPE(None, ctypes.POINTER(pfc_string))),
                ("get_about_message", ctypes.WINFUNCTYPE(None, ctypes.POINTER(pfc_string)))
                ]

Let’s Rock !

Ada dua parameter yang dibutuhkan oleh foobar2000_get_interface yaitu foobar2000_api dan HINSTANCE. Umumnya HINSTANCE adalah handle dari host yang didapat dari kernel32.GetModuleHandleA.

Sedangkan foobar2000_api implementasinya tidak akan ditemukan di SDK. Sehingga kita beri None. Semua function yang kita gunakan juga tidak ada yang menggunakannya (lihat SDK).

h_inst = ctypes.windll.kernel32.GetModuleHandleA(None)

component.foobar2000_get_interface.restype = ctypes.POINTER(foobar_client)

g_client = component.foobar2000_get_interface(None, h_inst)

print 'client-version', g_client.contents.vfptr.contents.get_version()

def hexbuffer(buf):
    return ''.join('{:02x}'.format(ord(x)) for x in buf)

componentversion_guid = 'bd3ebb10f7dd7549a3cc23084829453e'

s_now = g_client.contents.vfptr.contents.get_service_list()

while s_now:
    guid = s_now.contents.guid.contents

    if componentversion_guid == hexbuffer(buffer(guid)):        
        pfc   = pfc_string()
        cv = ctypes.cast(s_now.contents.vfptr_service, ctypes.POINTER(componentversion_vfptr))
        cv.contents.get_component_name(ctypes.byref(pfc))
        cv.contents.get_component_version(ctypes.byref(pfc))
        cv.contents.get_about_message(ctypes.byref(pfc))

        print g_strings
        break
    s_now = s_now.contents.next

How to get Foobar2000 component’s version – Part 1

Where the component’s version stored?

Komponen pada Foobar2000 berbentuk file dll (shared library). Pendistribusian komponen ini didalam file zip dengan ekstensi fb2k-component. Umumnya versi dari file dll dapat dilihat langsung melalui Properties > Details > Product version. Namun hal ini tidak ditemui untuk file dll komponen Foobar2000.

Artinya file dll tersebut di-compile tanpa menyertakan VERSIONINFO resource-nya. Agar dapat mengetahui versi-nya, komponen harus di-install pada Foobar2000 kemudian akan terlihat pada Preferences > Components.

Menurut Foobar2000 SDK cara mengisi versi komponen adalah dengan macro berikut (foo_sample\main.cpp):

DECLARE_COMPONENT_VERSION("Sample Component","1.0","about message goes here");

How Foobar2000 get the version?

Sebenarnya komponen pada Foobar2000 ini bisa diistilahkan sebagai module, extension, plug-in atau client yang hanya bisa diakses oleh host-nya yaitu aplikasi Foobar2000. Cara host komunikasi dengan client-nya adalah dengan memanggil suatu export function pada client yang sudah disepakati namanya. Export function inilah yang menjadi entrypoint dari sebuah komponen.

Menurut Foobar2000 SDK hanya ada satu entrypoint pada komponen yaitu foobar2000_get_interface yang akan me-return obyek foobar2000_client (foobar2000_component_client\component_client.cpp):

static foobar2000_client_impl g_client;

extern "C"
{
    __declspec(dllexport) foobar2000_client * _cdecl foobar2000_get_interface(foobar2000_api * p_api,HINSTANCE hIns)
    {
        g_hIns = hIns;
        g_foobar2000_api = p_api;

        return &g_client;
    }
}

How foobar2000 component designed?

Sebelum kita mendapatkan obyek foobar_client, kita membutuhkan dua argument atau parameter untuk fungsi foobar2000_get_interface yaitu dengan tipe HINSTANCE dan foobar2000_api. HINSTANCE adalah handle dari aplikasi host, dan biasanya digunakan jika ada keperluan dengan sistem operasi. foobar2000_api adalah interface yang diberikan host untuk client yang digukan salah satunya saat enumerasi service.

Komponen pada Foobar2000 terdiri dari beberapa service dengan kegunaan yang berbeda. Jumlah service suatu komponen dapat berbeda dengan lainnya tergantung fitur apa yang digunakan oleh komponen tersebut. Penyimpanan service ini dengan cara linked-list. Sehingga proses pengambilan service ini (enumerasi) dengan melakukan iterasi dari kepala (head) hingga ketemu buntut (tail).

Salah satu service yang kita butuhkan adalah componentversion. Karena kalau kita jabarkan macro DECLARE_COMPONENT_VERSION maka akan didapat (SDK\componentversion.h):

#define DECLARE_COMPONENT_VERSION(NAME,VERSION,ABOUT) \
    namespace {class componentversion_myimpl : public componentversion { public: componentversion_myimpl() {PFC_ASSERT( ABOUT );} \
        void get_file_name(pfc::string_base & out) {out = core_api::get_my_file_name();}    \
        void get_component_name(pfc::string_base & out) {out = NAME;}   \
        void get_component_version(pfc::string_base & out) {out = VERSION;} \
        void get_about_message(pfc::string_base & out) {out = ABOUT;}   \
        }; static service_factory_single_t<componentversion_myimpl> g_componentversion_myimpl_factory; }

Sehingga untuk mendapatkan versi komponen melalui fungsi componentversion_myimpl::get_component_version.

Macro ini mendifinisikan class componentversion_myimpl yang merupakan turunan dari componentversion (SDK\componentversion.h) yang juga merupakan turunan dari service_base (SDK\service.h).

Sedangkan obyek service akan instantiate (dibuat) oleh g_componentversion_myimpl_factory. Class dari variable g_componentversion_myimpl_factory yaitu service_factory_single_t (SDK\service.h) yang merupakan turunan dari service_factory_base_t (SDK\service.h) yang juga merupakan turunan service_factory_base (SDK\service.h).

Class service_factory_single_t juga yang akan meng-create service (sesuai T dalam hal ini componentversion_myimpl) dan disimpan pada member service_factory_single_t::g_instance.

Pembuatan variable g_componentversion_myimpl_factory juga akan mengeksekusi constructor service_factory_base::service_factory_base yang berfungsi untuk pengisian linked-list (service_factory_base::__internal__list dan service_factory_base::__internal__next) dari servis dan memberikan nilai GUID untuk servis tersebut.

GUID adalah nilai unik yang digunakan untuk identifikasi servis saat dienumerasi (SDK\guids.cpp). Untuk componentversion GUID-nya adalah:

// {10BB3EBD-DDF7-4975-A3CC-23084829453E}
FOOGUIDDECL const GUID componentversion::class_guid =
{ 0x10bb3ebd, 0xddf7, 0x4975, { 0xa3, 0xcc, 0x23, 0x8, 0x48, 0x29, 0x45, 0x3e } };

Kembali lagi ke obyek foobar_client. Sebenarnya obyek yang didapat dari foobar2000_get_interface adalah variable global g_client dengan class foobar2000_client_impl (foobar2000_component_client\component_client.cpp) yang merupakan turunan dari foobar2000_client (SDK\component.h). Dilihat fungsi foobar2000_client_impl::get_service_list maka akan diperoleh head dari linked-list servis yang ada pada komponen.

Conclusion

Untuk mendapatkan versi dari komponen dengan cara berikut:

  1. Ambil obyek dengan class foobar2000_client_impl dari entrypoint foobar2000_get_interface.
  2. Ambil head dengan class service_factory_base dari service melalui foobar2000_client_impl::get_service_list. Obyek sesungguhnya adalah service_factory_single_t namun nilai T belum pasti hingga kita menemukan nilai GUID-nya.
  3. Cocokkan nilai GUID-nya dengan componentversion::class_guid, jika tidak teruskan ke service_factory_base::__internal_next lalu ulangi step ini.
  4. Jika sudah cocok ambil obyek dengan class componentversion_myimpl dari service_factory_single_t::g_instance (dapat di-cast saja service_factory_base).
  5. Ambil versi komponent dari componentversion_myimpl::get_component_version.

Python Cffi Pointer to Pointer for Out Argument

Python Cffi is C Foreign Function Interface that make a call to a function inside compiled dynamic library (dll/so) made easy than Ctypes does. The pointer to pointer argument will need some tricks and fundamental about pointers in C.

Solusi untuk kasus ini sama persis dengan masalah pada LuaJIT.

struct Foo { int dummy; }
int tryToAllocateFoo(Foo ** dest);

Foo *pFoo = NULL;
tryToAllocateFoo(&pFoo);
if (pFoo) printf("dummy == %d", pFoo->dummy);
from cffi import FFI

ffi = FFI()

ffi.cdef("""
struct Foo { int dummy; };
int tryToAllocateFoo(Foo ** dest);
""")

lib = ffi.dlopen("foo.dll")

pFoo = ffi.new('struct Foo *[1]')
pFoo[0] = ffi.NULL
ok = lib.tryToAllocateFoo(pFoo)

if pFoo[0] != ffi.NULL:
print('dummy == %d' % (pFoo[0].dummy,))