블로그 사이트 옮김

지금까지 회사 서버에 몰래 기생해서 운영하던 lethean.pe.kr 블로그를 워드프레스닷컴 서비스로 이전했습니다. 새 블로그 사이트 이름은 sunjinyang.wordpress.com 입니다. 그리고 당분간 블로그는 더 이상 작성하지 않을 생각입니다. 사실 이제는, 직접 사이트를 구축하고 관리하는 작업이 부담스럽기도 하고 귀찮기도 한 게 사실이지만, 무엇보다도 시간이 모자라네요… 대신 이제부터는 요즘(?) 사람들처럼 이미 잘 구축된 다른 서비스를 이용해 볼 예정입니다.

lethean.pe.kr 사이트는 아마 수일 내에 사라지거나 도메인이 유효하다면 자동으로 sunjinyang.wordpress.com 사이트로 포워딩됩니다. 워드프레스닷컴으로 옮기면서 기존 글 모두 그대로 이전하는 작업은 쉬었지만, 글 내부에 존재하는 기존 사이트에 대한 링크는 유효하지 않게 되었습니다. 이 경우 아마도 맨 앞의 사이트 이름만 바꿔주면 정상적으로 접속이 가능하리라 생각합니다.

X 윈도우 마우스 포인터 장벽(barrier)

그놈3 데스크탑 환경을 사용할때 불편했던 점 중 하나는,  프로그램을 실행하는 등의 창 작업을 위해 화면 상단 왼쪽 구석으로 마우스를 이동했을때 듀얼 모니터에서 마우스가 미끌어져 버리는 현상이었습니다. 그런데, 어느 날부터인가 이 현상, 즉 마우스를 화면 구석으로 이동해도 마우스가 다음 모니터로 미끌어져 넘어가는 현상이 발생하지 않았습니다. 제가 ‘어느 날’이라는 표현을 사용한 이유는, 아치 리눅스로 이전한 이후로 아직까지 한 번도 시스템을 재설치하지 않고, 생각날 때마다 최소 2~3일에 한 번씩 패키지를 업그레이드하는 습관 때문입니다.

그러다가 최근 LWN.net의 X11R7.7 릴리스 기사에서 이런 내용을 읽게 되었습니다.

Pointer barriers were added by X Fixes extension Version 5.0. Compositing managers and desktop environments may have UI elements in particular screen locations such that for a single-headed display they correspond to easy targets, for example, the top left corner. For a multi-headed environment these corners should still be semi-impermeable. Pointer barriers allow the application to define additional constraint on cursor motion so that these areas behave as expected even in the face of multiple displays.

즉, X Fixes 확장 기능 5.0 버전에 포인터 장벽(?)이라는게 추가되었는데, 어플리케이션이 커서 움직임에 제한을 더할 수 있도록 했다는 겁니다. 그리고 이를 이용하면 바로 정확하게 제가 경험한 것과 같은 멀티 모니터에서의 구석 마우스 미끄러짐 현상을 없앨 수 있다는 점도 부연하고 있습니다.

사실 비슷한 문제가 제가 회사에서 개발 중인 프로그램에서도 발생하고 있었기 때문에 이 기능에 관심이 안 갈 수가 없었습니다. 그래서 조금 더 정확하게 확인하기 위해 그놈 셸 소스 코드를 조사했더니 아니나 다를까, 패널 박스 크기가 변경될때마다(“allocation-changed”) 호출되는 _updatePanelBarriers() 함수가 그 역할을 하고 있습니다. (2011년 7월 25일에 작성된 코드군요…)

_updatePanelBarriers: function() {
    if (this._leftPanelBarrier)
        global.destroy_pointer_barrier(this._leftPanelBarrier);
    if (this._rightPanelBarrier)
        global.destroy_pointer_barrier(this._rightPanelBarrier);

    if (this.panelBox.height) {
        let primary = this.primaryMonitor;
        this._leftPanelBarrier =
            global.create_pointer_barrier(primary.x, primary.y,
                                          primary.x, primary.y + this.panelBox.height,
                                          1 /* BarrierPositiveX */);
        this._rightPanelBarrier =
            global.create_pointer_barrier(primary.x + primary.width, primary.y,
                                          primary.x + primary.width, primary.y + this.panelBox.height,
                                          4 /* BarrierNegativeX */);
    } else {
        this._leftPanelBarrier = 0;
        this._rightPanelBarrier = 0;
    }
}

위 자바스크립트 코드가 호출하는 실제 C 함수 코드는 다음과 같습니다.

/**
 * shell_global_create_pointer_barrier:
 * @global: a #ShellGlobal
 * @x1: left X coordinate
 * @y1: top Y coordinate
 * @x2: right X coordinate
 * @y2: bottom Y coordinate
 * @directions: The directions we're allowed to pass through
 *
 * If supported by X creates a pointer barrier.
 *
 * Return value: value you can pass to shell_global_destroy_pointer_barrier()
 */
guint32
shell_global_create_pointer_barrier (ShellGlobal *global,
                                     int x1, int y1, int x2, int y2,
                                     int directions)
{
#if HAVE_XFIXESCREATEPOINTERBARRIER
  return (guint32)
    XFixesCreatePointerBarrier (global->xdisplay,
                                DefaultRootWindow (global->xdisplay),
                                x1, y1,
                                x2, y2,
                                directions,
                                0, NULL);
#else
  return 0;
#endif
}

/**
 * shell_global_destroy_pointer_barrier:
 * @global: a #ShellGlobal
 * @barrier: a pointer barrier
 *
 * Destroys the @barrier created by shell_global_create_pointer_barrier().
 */
void
shell_global_destroy_pointer_barrier (ShellGlobal *global, guint32 barrier)
{
#if HAVE_XFIXESCREATEPOINTERBARRIER
  g_return_if_fail (barrier > 0);

  XFixesDestroyPointerBarrier (global->xdisplay, (PointerBarrier)barrier);
#endif
}

XFixesCreatePointerBarrier() / XFixesDestroyPointerBarrier() 함수에 대한 더 자세한 사용법을 확인하기 위해 XFIXES 공식 프로토콜 문서를 확인해 보니 마지막에 다음과 같은 API 설명이 있습니다.

12. Pointer Barriers

...

12.1 Types

	BARRIER:	XID

	BarrierDirections

		BarrierPositiveX:	    1 << 0
		BarrierPositiveY:	    1 << 1
		BarrierNegativeX:	    1 << 2
		BarrierNegativeY:	    1 << 3

12.3 Requests

CreatePointerBarrier

		barrier:		    BARRIER
		drawable:		    DRAWABLE
		x1, y2, x2, y2:		    INT16
		directions:		    CARD32
		devices:		    LISTofDEVICEID

	Creates a pointer barrier along the line specified by the given
	coordinates on the screen associated with the given drawable.  The
	barrier has no spatial extent; it is simply a line along the left
	or top edge of the specified pixels.  Barrier coordinates are in
	screen space.

	The coordinates must be axis aligned, either x1 == x2, or
	y1 == y2, but not both.  The varying coordinates may be specified
	in any order.  For x1 == x2, either y1 > y2 or y1 < y2 is valid.
	If the coordinates are not valid BadValue is generated.

	Motion is allowed through the barrier in the directions specified:
	setting the BarrierPositiveX bit allows travel through the barrier
	in the positive X direction, etc.  Nonsensical values (forbidding Y
	axis travel through a vertical barrier, for example) and excess set
	bits are ignored.

	If the server supports the X Input Extension version 2 or higher,
	the devices element names a set of master device to apply the
	barrier to.  If XIAllDevices or XIAllMasterDevices are given, the
	barrier applies to all master devices.  If a slave device is named,
	BadDevice is generated; this does not apply to slave devices named
	implicitly by XIAllDevices.  Naming a device multiple times is
	legal, and is treated as though it were named only once.  If a
	device is removed, the barrier continues to apply to the remaining
	devices, but will not apply to any future device with the same ID
	as the removed device.  Nothing special happens when all matching
	devices are removed; barriers must be explicitly destroyed.

	Errors: IDChoice, Window, Value, Device

DestroyPointerBarrier

		barrier:		    BARRIER

	Destroys the named barrier.

포인터 장벽은 반드시 화면 구석에서만 사용할 수 있는 게 아니라 화면 어느 곳에나 생성할 수 있으며, 장벽 생성시 지정한 방향(direction)으로만 마우스 커서가 이동할 수 있도록 허용합니다. 다시 말해 마우스 커서가 반대 방향으로는 장벽을 넘어갈 수 없게 합니다.방향과 함께 장벽의 영역 좌표를 지정해야 하는데, 예를 들어 왼쪽에서 오른쪽이라면 Y 좌표값만 다르고 X 좌표값을 동일하게 지정해야 합니다. 즉, 세로로 장벽 선을 그리면 됩니다. 그런데, 사실 화면 구석에서는 원하는 대로 동작하는데, 화면 임의의 위치에 포인터 장벽을 생성해 보면 마우스 커서의 대각선 움직임 등은 허용하기 때문에 아주 정확하게 원하는 대로 동작하지 않을 수도 있습니다.

참고로 상위 툴킷에서 X 윈도우 API 호출에 사용하는 Display, Window 핸들을 얻으려면, Clutter의 경우 clutter_x11_get_default_display() / clutter_x11_get_stage_window() 또는 clutter_x11_get_root_window() 함수를 이용하면 됩니다. GTK+ 역시 GDK 관련 API를 뒤져 보시면 됩니다. ;)

Evolus Pencil 프로토타이핑 도구

GUI 설계 단계에서 사용하는 프로토타이핑(prototyping) 도구는 매우 많습니다. 그냥 김프(Gimp)나 포토샵, 잉크스케이프(Inkscape) 등과 같은 일반적인 그래픽 도구에 익숙한 사람에게는 더 이상의 도구가 필요없겠지만, 저처럼 디자인에 문외한인 프로그래머에게는 더 쉽고 자동화된 도구가 필요할 수 밖에 없습니다. 그래서, 그냥 오픈오피스나 파워포인트, 워드 등과 같은 오피스 슈트를 이용해서 그린 적도 있었고, 조금 더 특화된 비지오(Visio), 다이아(Dia) 등을 이용하기도 하다가 최근까지는 발사믹 목업(Balsamiq Mockups) 등과 같은 도구를 이용했습니다.

리눅스를 기본 데스크탑으로 사용하면서 업무상 어쩔 수 없이 가끔 맥 / 윈도우를 사용하다보니 유료 소프트웨어를 사용하면 웬지 돈이 아깝다는 생각이 들 때가 있습니다. 리눅스에서는 필요한 소프트웨어를 대부분 그때 그때 패키지를 검색해서 설치하고, 배포판 패키지가 없으면 소스 가져다 컴파일 해서 사용하는 습관이 오랫동안 길들여져 있기도 하지만, 리눅스용 소프트웨어는 대부분 오픈 소스일 거라는 편견(!) 때문에, 가능하면 기능이 조금 부족하더라도, 어차피 제가 전문적으로 사용하지 않는다면, 유료 소프트웨어에 대한 오픈 소스 / 무료 소프트웨어 대안이 있으면 이를 사용하는 편입니다. (소프트웨어 개발로 밥먹는 프로그래머의 마인드가 이러면 안된다는 걸 알기에, 적어도 최소한의 양심으로, 불법복제 만큼은 피하고 있습니다)

에볼루스 펜슬(Evolus Pencil)은 이러한 개인적인 취향에 딱 맞는 도구입니다. 한 블로그 글에서 처음 접하게 되었는데, 알고 보니 꽤 유명한 도구입니다. 오픈 소스이면서 무료인 것은 물론, 리눅스 / 윈도우 / 맥 플랫폼을 모두 지원합니다. 또한 독립 어플리케이션처럼 실행할 수도 있고 파이어폭스 확장 기능으로 사용할 수도 있습니다. 작업한 내용을 HTML, PNG, PDF, ODT, SVG 형식으로 저장할 수도 있습니다. 그리고, 유료 도구인 발사믹 목업(Balsamiq Mockups)만큼 다양하지는 않지만 다운로드해서 쉽게 추가할 수 있는 스텐실(stencil)과 템플릿(template)도 몇몇 제공합니다. 참고로, 공식 스텐실 다운로드 페이지에는 없는 다른 스텐실, 예를 들어 이전 버전 안드로이드나 iOS 스텐실은 다운로드 목록을 잘 찾아보면 발견할 수 있습니다. 아마도 버전이 올라가면서 이전 버전용 스텐실이 잘 호환되지 않아서 링크가 사라진 것 같습니다.

우리말로 이미 잘 소개된 동영상 튜토리얼이 있기 때문에 더 자세한 설명은 생략합니다. 도움 되시길~

참고로, 위의 폭포 사진은 엊그제 다녀온 강원도 철원 삼부연 폭포 전경입니다.

리눅스 IP 주소 / 링크 상태 변경 여부 감지하기

리눅스에서 IP 주소가 변경되었거나 링크 상태 변경 여부(예를 들어 랜선이 꽂히거나 빠졌을때)를 자동으로 감지하는 C 코드입니다. ifconfig 명령등의 결과를 파싱하는 방법이 아닌 리눅스 커널 rtnetlink(7) 프로토콜과 getifaddrs() 함수를 이용해 직접 처리합니다. 참조한 소스는 여러군데가 있는데 모두 구글링이 가능하므로 결과물만 기록으로 남겨둡니다.

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <ifaddrs.h>
#include <net/if.h>
#include <netdb.h>
#include <netinet/in.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>

static int
create_sock (const char *nic)
{
  struct sockaddr_nl addr;
  int                sock;

  memset (&addr, 0, sizeof (addr));
  addr.nl_family = AF_NETLINK;
  addr.nl_groups = RTMGRP_LINK | RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR;

  sock = socket (PF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
  if (sock < 0)
    {
      fprintf (stderr, "failed to open NETLINK_ROUTE socket for %s - %s(%d)",
               nic, strerror (errno), errno);
      return -1;
    }

  if (bind (sock, (struct sockaddr *)&addr, sizeof(addr)) < 0)
    {
      fprintf (stderr, "failed to bind NETLINK_ROUTE socket for %s - %s(%d)",
                 nic, strerror (errno), errno);
      close (sock);
      return -1;
    }

  return sock;
}

static int
ip_changed (int         sock,
            const char *nic)
{
  struct nlmsghdr   *nlh;
  char               buffer[4096];
  int                len;
  int                idx;
  int                found;

  len = recv (sock, buffer, sizeof (buffer), 0);
  if (len <= 0)
    {
      fprintf (stderr, "NETLINK_ROUTE socket recv() failedn");
      return -1;
    }

  found = 0;
  idx = if_nametoindex (nic);

  for (nlh = (struct nlmsghdr *)buffer;
       NLMSG_OK (nlh, len);
       nlh = NLMSG_NEXT (nlh, len))
    {
      if (nlh->nlmsg_type == NLMSG_DONE)
        break;
      if (nlh->nlmsg_type == NLMSG_ERROR)
        continue;
      if (!(NLMSG_OK (nlh, len)))
        continue;

      switch (nlh->nlmsg_type)
        {
        case RTM_NEWADDR:
          {
            struct ifaddrmsg *ifa = (struct ifaddrmsg *)NLMSG_DATA (nlh);

            if (ifa->ifa_index == idx)
              found = 1;
          }
          break;
        case RTM_NEWLINK:
          {
            struct ifinfomsg *ifi = (struct ifinfomsg *)NLMSG_DATA (nlh);

            if (ifi->ifi_index == idx)
              found = 1;
          }
          break;
        default:
          break;
        }
    }

  return found;
}

static int
get_nic_addr (const char     *nic,
              struct ifaddrs *ifaddr,
              int             wanted_family,
              char           *host,
              int             host_len,
              int            *active)
{
  struct ifaddrs *ifa;

  for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next)
    {
      int family;
      int s;

      if (ifa->ifa_addr == NULL)
        continue;

      if (strcmp (ifa->ifa_name, nic))
        continue;

      /* Skip unwanted families. */
      family = ifa->ifa_addr->sa_family;
      if (family != wanted_family)
        continue;

      *active = (ifa->ifa_flags & IFF_RUNNING) ? 1 : 0;

      s = getnameinfo (ifa->ifa_addr,
                       family == AF_INET ? sizeof (struct sockaddr_in) :
                                           sizeof (struct sockaddr_in6),
                       host,
                       host_len,
                       NULL,
                       0,
                       NI_NUMERICHOST);
      if (s != 0)
        {
          fprintf (stderr, "failed to getnameinfo() for '%s - %s(%d)",
                   ifa->ifa_name, strerror (errno), errno);
          continue;
        }

      /* Get the address of only the first network interface card. */
      return 1;
    }

  return 0;
}

static void
print_ip (const char *nic)
{
  struct ifaddrs *ifaddr;
  char            addr[NI_MAXHOST];
  int             active;

  if (getifaddrs (&ifaddr) == -1)
    {
      fprintf (stderr, "failed to getifaddrs() - %s(%d)", strerror (errno), errno);
      return;
    }

  if (!get_nic_addr (nic, ifaddr, AF_INET, addr, sizeof (addr), &active))
    if (!get_nic_addr (nic, ifaddr, AF_INET6, addr, sizeof (addr), &active))
      {
        strcpy (addr, "127.0.0.1");
        active = 0;
      }

  freeifaddrs (ifaddr);

  fprintf (stdout, "%s is %s (link %s)n",
           nic, addr, active ? "active" : "inactive");
}

int
main (void)
{
  char *nic = "eth0";
  int   sock;

  print_ip (nic);

  sock = create_sock (nic);
  if (sock < 0)
    return -1;

  while (1)
    {
      int ret;

      ret = ip_changed (sock, nic);
      if (ret < 0)
        return -1;

      if (ret)
        print_ip (nic);
    }

  close (sock);

  return 0;
}

/*
  Local Variables:
   mode:c
   c-file-style:"gnu"
   indent-tabs-mode:nil
  End:
  vim:autoindent:filetype=c:expandtab:shiftwidth=2:softtabstop=2:tabstop=8
*/

참고로 위 소스에서 네트웍 인터페이스 설정 변경을 감지하기 위해 사용한 소켓 파일 디스크립터(socket file descriptor)는 select() / poll() 등을 이용해 비동기적으로 감시하는 것도 가능합니다. 당연하지만, GLib 메인루프g_io_add_watch() 등을 이용해도 됩니다.

[UPDATE 2012-03-21] rtnetlink(7) 프로토콜의 기반이 되는 netlink(7) 프로토콜에 대해 더 자세히 알고 싶다면 Netlink 라이브러리의 Netlink 프로토콜 기초 문서를 참고하기 바랍니다.

GObject 객체 지향 프로그래밍 (5)

거의 2년만에 GObject 객체 지향 프로그래밍 연재 글을 포스팅합니다. 사실 이 글의 일부는 예전에 작성해 둔 것인데, 이번 GNOME Tech Talks에서 발표 하나를 맡게 되면서, 슬라이드 자료를 따로 만들 시간은 없고 그렇다고 오래된 자료를 재탕하는 건 실례인 것 같아 조금 보완해서 작성했습니다. 참고로, GObject 개념을 잘 모르는 분이라면 이전 연재 글을 먼저 읽어 보시면 도움이 될 수 있습니다. :)

  1. GObject 객체 지향 프로그래밍 (1)
  2. GObject 객체 지향 프로그래밍 (2)
  3. GObject 객체 지향 프로그래밍 (3)
  4. GObject 객체 지향 프로그래밍 (4)
  5. 싱글턴(Singleton) GObject 객체 만들기
  6. GObject 속성 직렬화(Serialization)하기

GObject 객체 지향 시스템을 구성하는 여러가지 개념 중 상속(inheritance), 참고 카운터(reference counting), 속성(properties) 등에 대해서는 지난 글에서 이미 소개했습니다. 아직 GObject 라이브러리에서 소개하지 않은 개념이 아직 많이 남아 있지만, 그 중에서 가장 중요한 것 중 하나는 바로 시그널(signals)이 아닐까 생각합니다. 속성이 변경되었을때 자동으로 호출되는 콜백 함수를 등록해서 사용하는 방법을 설명할 때 약간 소개했지만, 아무래도 그걸로는 부족하기 때문에 이번 글은 시그널의 개념과 사용 방법, 그리고 속성 바인딩을 정리해 보았습니다.

간단한 클러터 기반 시계

언제나 그렇듯이 재미없는 예제 소스를 먼저 보여드립니다. 이 소스를 컴파일해서 실행하면 위 그림과 같은 시계가 동작합니다.

/* myclock1.c */

/*****************************************************************************/

#include <glib-object.h>

#define MY_TYPE_CLOCK (my_clock_get_type ())
#define MY_CLOCK(obj) 
  (G_TYPE_CHECK_INSTANCE_CAST ((obj), MY_TYPE_CLOCK, MyClock))
#define MY_CLOCK_CLASS(klass) 
  (G_TYPE_CHECK_CLASS_CAST ((klass), MY_TYPE_CLOCK, MyClockClass))
#define MY_IS_CLOCK(obj) 
  (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MY_TYPE_CLOCK))
#define MY_IS_CLOCK_CLASS(klass) 
  (G_TYPE_CHECK_CLASS_TYPE ((klass), MY_TYPE_CLOCK))
#define MY_CLOCK_GET_CLASS(obj) 
  (G_TYPE_INSTANCE_GET_CLASS ((obj), MY_TYPE_CLOCK, MyClockClass))

typedef struct _MyClock        MyClock;
typedef struct _MyClockClass   MyClockClass;
typedef struct _MyClockPrivate MyClockPrivate;

struct _MyClock
{
  GObject         parent;
  MyClockPrivate *priv;
};

struct _MyClockClass
{
  GObjectClass parent_class;
};

enum
{
  PROP_0,
  PROP_DATE_TIME,
  PROP_LAST
};

struct _MyClockPrivate
{
  GDateTime *datetime;
  guint      timeout;
};

G_DEFINE_TYPE (MyClock, my_clock, G_TYPE_OBJECT);

static GParamSpec *props[PROP_LAST];

GDateTime *
my_clock_get_date_time (MyClock *clock_)
{
  g_return_val_if_fail (MY_IS_CLOCK (clock_), NULL);

  return g_date_time_ref (clock_->priv->datetime);
}

static void
my_clock_set_date_time (MyClock   *clock_,
                        GDateTime *datetime)
{
  g_date_time_unref (clock_->priv->datetime);
  clock_->priv->datetime = g_date_time_ref (datetime);
  g_object_notify_by_pspec (G_OBJECT (clock_), props[PROP_DATE_TIME]);
}

static gboolean
my_clock_update (gpointer data)
{
  MyClock   *clock_ = data;
  GTimeVal   now;
  GDateTime *datetime;
  guint      interval;

  g_get_current_time (&now);

  datetime = g_date_time_new_from_timeval_local (&now);
  my_clock_set_date_time (clock_, datetime);
  g_date_time_unref (datetime);

  interval = (1000000L - now.tv_usec) / 1000L;
  clock_->priv->timeout =
    g_timeout_add_full (G_PRIORITY_HIGH_IDLE,
                        interval,
                        my_clock_update,
                        g_object_ref (clock_),
                        g_object_unref);

  return FALSE;
}

static void
my_clock_set_property (GObject      *object,
                       guint         param_id,
                       const GValue *value,
                       GParamSpec   *pspec)
{
  switch (param_id)
    {
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
      break;
    }
}

static void
my_clock_get_property (GObject   *object,
                       guint      param_id,
                       GValue     *value,
                       GParamSpec *pspec)
{
  MyClock *clock_ = MY_CLOCK (object);

  switch (param_id)
    {
    case PROP_DATE_TIME:
      g_value_set_boxed (value, clock_->priv->datetime);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
      break;
    }
}

static void
my_clock_finalize (GObject *gobject)
{
  MyClockPrivate *priv = MY_CLOCK (gobject)->priv;

  g_date_time_unref (priv->datetime);
  g_source_remove (priv->timeout);

  G_OBJECT_CLASS (my_clock_parent_class)->finalize (gobject);
}

static void
my_clock_class_init (MyClockClass *klass)
{
  GObjectClass *obj_class = G_OBJECT_CLASS (klass);
  GParamSpec   *pspec;

  obj_class->set_property = my_clock_set_property;
  obj_class->get_property = my_clock_get_property;
  obj_class->finalize     = my_clock_finalize;

  g_type_class_add_private (klass, sizeof (MyClockPrivate));

  pspec = g_param_spec_boxed ("datetime",
                              "Date and Time",
                              "The date and time to show in the clock",
                              G_TYPE_DATE_TIME,
                              G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
  props[PROP_DATE_TIME] = pspec;
  g_object_class_install_property (obj_class, PROP_DATE_TIME, pspec);
}

static void
my_clock_init (MyClock *clock_)
{
  MyClockPrivate *priv;

  priv = clock_->priv =
    G_TYPE_INSTANCE_GET_PRIVATE (clock_,
                                 MY_TYPE_CLOCK,
                                 MyClockPrivate);

  priv->datetime = g_date_time_new_now_local ();
  priv->timeout = 0;

  my_clock_update (clock_);
}

MyClock *
my_clock_new (void)
{
  return g_object_new (MY_TYPE_CLOCK, NULL);
}

/*****************************************************************************/

#include <clutter/clutter.h>

static void
clock_datetime_changed (GObject    *object,
                        GParamSpec *pspec,
                        gpointer    data)
{
  MyClock      *clock_ = MY_CLOCK (object);
  ClutterActor *text   = data;
  GDateTime    *datetime;
  gchar        *str;

  datetime = my_clock_get_date_time (clock_);
  str = g_date_time_format (datetime, "%xn%H:%M:%S");

  clutter_text_set_text (CLUTTER_TEXT (text), str);

  g_free (str);
  g_date_time_unref (datetime);
}

int
main (int    argc,
      char **argv)
{
  ClutterActor      *stage;
  ClutterActor      *text;
  ClutterConstraint *constraint;
  MyClock           *clock_;

  if (clutter_init (&argc, &argv) != CLUTTER_INIT_SUCCESS)
    return -1;

  /* stage */
  stage = clutter_stage_get_default ();
  clutter_actor_set_size (stage, 320, 240);
  clutter_stage_set_color (CLUTTER_STAGE (stage), CLUTTER_COLOR_Black);
  clutter_stage_set_user_resizable (CLUTTER_STAGE (stage), TRUE);

  /* text */
  text = clutter_text_new_full ("Sans Bold 20",
                                "NOW",
                                CLUTTER_COLOR_LightButter);
  clutter_container_add_actor (CLUTTER_CONTAINER (stage), text);
  clutter_text_set_line_alignment (CLUTTER_TEXT (text), PANGO_ALIGN_CENTER);

  /* align text in center of stage */
  constraint =
    clutter_align_constraint_new (stage, CLUTTER_ALIGN_X_AXIS, 0.5);
  clutter_actor_add_constraint (text, constraint);

  constraint =
    clutter_align_constraint_new (stage, CLUTTER_ALIGN_Y_AXIS, 0.5);
  clutter_actor_add_constraint (text, constraint);

  /* clock */
  clock_ = my_clock_new ();
  g_signal_connect (clock_,
                    "notify::datetime",
                    G_CALLBACK (clock_datetime_changed),
                    text);

  clutter_actor_show (stage);

  clutter_main ();

  return 0;
}

소스 코드를 간단하게 설명하면, MyClock 객체가 1초 간격으로 현재 시간을 얻어와 자신의 datetime 속성을 갱신하면[my_clock_update()], 속성이 변경되었을때(notify::datetime) 자동으로 호출되는 콜백 함수를[clock_datetime_changed()] 등록해 자동으로 클러터 텍스트(ClutterText)를 이용해 화면에 표시합니다.

이제 이 소스 코드를 두 가지 방법으로 확장하려고 합니다. 첫번째 방법은 속성 바인딩(property binding)을 이용해 시그널을 사용하지 않는 방법이고, 두번째 방법은 시간이 변경되었을때 호출되는 진짜(!) 시그널을 추가하는 것입니다.

속성 바인딩 (Property Binding)

속성 바인딩(property binding)이란 두 GObject 객체간의 두 속성을 묶는 걸 말합니다. 여기서 묶는다는 의미는, 한 객체의 속성 값이 변하면 다른 객체의 속성 값도 자동으로 변한다는 의미입니다. 물론 묶으려는 두 속성은 같은 형(type)이어야 합니다. 그런데, 위 예제의 경우 MyClock:datetime 속성과 ClutterText:text 속성은 형(type)이 다릅니다. 그래서, 위 소스를 다음과 같이 수정합니다. (변경된 부분만 보여 드립니다)

/* myclock2.c */

/* ... */

enum
{
  PROP_0,
  PROP_DATE_TIME,
  PROP_TEXT,
  PROP_LAST
};

struct _MyClockPrivate
{
  GDateTime *datetime;
  guint      timeout;
  gchar     *text;
};

/* ... */

const gchar *
my_clock_get_text (MyClock *clock_)
{
  g_return_val_if_fail (MY_IS_CLOCK (clock_), NULL);

  return clock_->priv->text;
}

static void
my_clock_set_date_time (MyClock   *clock_,
                        GDateTime *datetime)
{
  g_date_time_unref (clock_->priv->datetime);
  clock_->priv->datetime = g_date_time_ref (datetime);
  g_object_notify_by_pspec (G_OBJECT (clock_), props[PROP_DATE_TIME]);

  g_free (clock_->priv->text);
  clock_->priv->text = g_date_time_format (datetime, "%xn%H:%M:%S");
  g_object_notify_by_pspec (G_OBJECT (clock_), props[PROP_TEXT]);
}

/* ... */

static void
my_clock_finalize (GObject *gobject)
{
  /* ... */
  g_free (priv->text);
  /* ... */
}

static void
my_clock_class_init (MyClockClass *klass)
{
  /* ... */

  pspec = g_param_spec_string ("text",
                               "Text",
                               "The text of the date and time",
                               NULL,
                               G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
  props[PROP_TEXT] = pspec;
  g_object_class_install_property (obj_class, PROP_TEXT, pspec);
}

static void
my_clock_init (MyClock *clock_)
{
  /* ... */
  priv->text = NULL;
  /* ... */
}

/* ... */

int
main (int    argc,
      char **argv)
{
  /* ... */

  /* clock */
  clock_ = my_clock_new ();
  g_object_bind_property (clock_, "text",
                          text,  "text",
                          G_BINDING_SYNC_CREATE);

  /* ... */
}

위 코드에서 변경된 내용은, MyClock에 문자열 형식의 text 속성을 추가하고[my_clock_class_init()], datetime 속성을 갱신할때 text 속성도 함께 갱신하도록 한 다음[my_clock_set_date_time()], 기존 속성 변경(notify::datetime)에 대한 g_signal_connect() 함수 호출 대신 g_object_bind_property() 함수를 이용해 두 객체의 속성을 묶었다는 점입니다. 여기서 핵심은 물론 g_object_bind_property() 함수인데, 이 함수는 GLib 2.26 버전에 추가되었으며 예전에 소개한 ExoBinding과 사용법이 거의 유사합니다. 물론, 옵션을 통해 바인딩하는 시점부터 값을 동기화할 지(G_BINDING_SYNC_CREATE), 단방향이 아닌 양방향으로 동기화할 지(G_BINDING_BIDIRECTIONAL) 등을 지정할 수도 있습니다. 이처럼, 위의 코드에서 볼 수 있듯이, 속성 바인딩을 이용하면 매번 콜백함수를 만들지 않고도 간단하게 코드 몇 줄로 원하는 객체 속성간의 동기화(synchronization)를 처리할 수 있습니다.

여담이지만, 처음 이 기능을 접했을때 맥, 아이폰 응용 프로그램을 개발하기 위해 XCode에서 마우스 드래그 만으로 객체 속성간 바인딩이 지원되는 것처럼, 코딩이 아닌, Glade 같은 GUI 도구에서 위젯 속성간 바인딩이 지원되면 참 편하지 않을까 하는 생각이 들었던 적도 있습니다.

시그널 (Signals)

GObject 공식 매뉴얼에 의하면 시그널(signals)은 메시지 전달 시스템을 구성하는 두가지 기술 중 하나입니다. 하나는 클로저(closures)이고 다른 하나가 시그널(signals)인데, 클로저가 콜백(callback) 함수를 자료구조로 정의한 거라면, 시그널은 이 콜백함수를 등록하고 호출하는 알고리즘을 정의한 것이라고 이해해도 무방합니다.

클로저를 다시 정의하지 않고 함수 포인터를 직접 사용해도 될 것 같은데 이를 객체로 정의한 이유는 여러가지가 있지만, 무엇보다도 콜백함수에 전달되는 인자(parameters) 목록과 인자 형(type)에 대한 처리(marshalling) 때문입니다. C/C++ 언어에서 함수 호출시 스택에 쌓이는 인자를 가공하는 것 뿐 아니라, GObject가 지원하는 여러 언어에 대한 바인딩을 위해 더 일반화된 클로저(closure) 객체가 필요합니다.

아무튼, 이론적인 설명은 그만하고 다시 본론으로 돌아와서, 위 예제에서 구현한 MyClock 객체가 생각보다 잘 설계되고 동작하는 바람에(…) 프로그램 전체에서 이 객체를 사용하기로 결정했다고 가정해 봅시다. 수많은 모듈과 수많은 객체에서 전역 시계 객체에 속성 알림(notify) 시그널을 연결합니다. 그리고 그때마다 my_clock_get_date_time()을 호출해 현재 시간을 가져와서 처리합니다. 물론 이 예제에서 전달되는 GDateTime 구조체는 참조 카운터 방식으로 관리되기 때문에 구조체 전달시  많은 오버헤드가 없지만, 문자열을 복사하거나 많은 데이터가 전달되는 경우라면 무시할 수 없는 상황이 발생합니다. 그래서, 위 첫번째 소스를 다음과 같이 조금 수정합니다.

/* myclock3.c */

/* ... */

struct _MyClockClass
{
  GObjectClass parent_class;

  /* signals */
  void (*changed) (MyClock   *clock_,
                   GDateTime *datetime);
};

enum
{
  SIGNAL_CHANGED,
  SIGNAL_LAST
};

/* ... */

static guint       signals[SIGNAL_LAST];

/* ... */

static void
my_clock_set_date_time (MyClock   *clock_,
                        GDateTime *datetime)
{
  /* ... */
}

static void
my_clock_real_changed (MyClock   *clock_,
                       GDateTime *datetime)
{
  my_clock_set_date_time (clock_, datetime);
}

static gboolean
my_clock_update (gpointer data)
{
  /* ... */

  datetime = g_date_time_new_from_timeval_local (&now);
  g_signal_emit (clock_, signals[SIGNAL_CHANGED], 0, datetime);
  g_date_time_unref (datetime);

  /* ... */
}

static void
my_clock_class_init (MyClockClass *klass)
{
  /* ... */

  klass->changed = my_clock_real_changed;

  signals[SIGNAL_CHANGED] =
    g_signal_new ("changed",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (MyClockClass, changed),
                  NULL,
                  NULL,
                  g_cclosure_marshal_VOID__POINTER,
                  G_TYPE_NONE,
                  1,
                  G_TYPE_POINTER);
}

/* ... */

static void
clock_changed (MyClock   *clock_,
               GDateTime *datetime,
               gpointer   user_data)
{
  ClutterActor *text = user_data;
  gchar        *str;

  str = g_date_time_format (datetime, "%xn%H:%M:%S");
  clutter_text_set_text (CLUTTER_TEXT (text), str);
  g_free (str);
}

int
main (int    argc,
      char **argv)
{
  /* ... */

  /* clock */
  clock_ = my_clock_new ();
  g_signal_connect (clock_,
                    "changed",
                    G_CALLBACK (clock_changed),
                    text);

  /* ... */
}

바로 위 코드에 보이는 것처럼 g_signal_connect() 호출시 연결하는 시그널 이름과 콜백 함수[clock_changed()]가 더 단순하고 효율적으로 변경된 걸 확인할 수 있습니다. 콜백 함수 호출시 전달되는 인수를 그냥 사용하면 되니까 오버헤드가 매우 많이 줄어들 수 밖에 없습니다. 하지만 시그널을 정의해서 사용하는게 단순히 성능과 효율 때문만은 아닙니다. 위 예제에서는 속성이 변경되었을 때 발생하는 시그널을 정의했지만, 일반적으로 시그널은 속성 만으로 표현할 수 없는 객체의 상태 변화를 알리기 위해서 많이 사용합니다.(예: ClutterActor::enter-event 시그널) 또한 속성의 변화를 통해 알 수 있더라도 더 쉽고 명확하게 이를 전파하기 위해서도 사용합니다.(예: ClutterActor::hide 시그널과 ClutterActor:visible 속성)

더 나아가, 시그널은 상태 변화 뿐 아니라 객체의 동작 방식을 외부에서 제어할 수 있도록 유연성을 제공하는데도 사용합니다. 더 자세한 이해를 위해 시그널 함수 포인터부터 설명하자면, 클래스 구조체 안에 선언된 시그널 함수 포인터[MyClockClass::changed()]는 일종의 가상 함수(virtual function) 역할을 하면서, 시그널이 발생하면(emit) g_signal_connect()를 이용해 등록된 사용자 콜백함수가 모두 실행된 뒤 맨 나중에 실행되거나 혹은 사용자 콜백 함수보다 먼저 실행됩니다. 따라서 필요 없을 경우 그냥 NULL로 내버려두어도 상관없지만, 위 예제에서는 클래스 생성시 my_clock_real_changed() 함수를 등록시켰습니다. my_clock_real_changed()는 다시  실제로 datetime 속성을 갱신하는 작업을 처리하는 my_clock_set_date_time()을 호출합니다. 그리고, 기존 시간 갱신 함수[my_clock_update()]에서는 직접 my_clock_set_date_time()을 호출하지 않고, 시그널을 발생시켜[g_signal_emit()] 작업을 처리합니다.

왜 이렇게 복잡하게 일을 나누어 처리할까요? 이렇게 구현하면 몇 가지 장점이 있기 때문입니다. 예를 들어 위 예제에서는 datetime 속성이 읽기 전용으로 선언되어 있기 때문에 외부에서 그 값을 변경할 수 없습니다. 하지만, 외부에서 직접 g_signal_emit_by_name() 등을 이용해 시그널을 발생시키면 시그널에 연결된 모든 콜백 함수 뿐 아니라 my_clock_real_changed() 함수까지도 간접적으로 호출되어 작업을 처리하도록 할 수 있습니다. 게다가 만일 시그널에 연결된 콜백 함수 중 하나가 어떤 이유로 g_signal_stop_emission_by_name() 등을 호출하면 이후 실행될 콜백 함수나 my_clock_real_changed() 함수가 호출되지 않게 할 수도 있고, 심지어 객체의 클래스에 등록된 함수 포인터에 직접 자신만의 콜백 함수를 등록해서 원래 작업이 아예 수행되지 않게 할 수도 있습니다.

참고로, GTK+ / Clutter 등과 같은 GObject 기반 그래픽 툴킷 시스템은 대부분 이 시그널 콜백 함수 메커니즘을 이용해 커스텀 위젯을 만들거나 기존 액터를 상속받아 사용자가 마음껏 기능을 확장할 수 있는 길을 열어 두었습니다.(예: clutter_actor.c:clutter_actor_real_paint() 소스 참고)

시그널 객체는 g_signal_new() 함수를 이용해 생성한 뒤 전역 signals[] 배열에 ID를 저장해 둡니다. 이렇게 저장한 시그널 ID는 g_signal_emit() 함수 호출시 사용합니다. 물론 이렇게 ID를 따로 저장하지 않고 g_signal_emit_by_name()을 사용해 시그널 이름으로 직접 시그널을 발생시켜도 되지만, 어차피 내부적으로 시그널 이름을 ID로 변환하는 과정을 거치기 때문에 효율을 위해 객체 구현시 관례적으로 이런식으로 작성합니다. 물론 객체 외부에서는 시그널 ID를 모르기 때문에 어쩔 수 없이  g_signal_emit_by_name()을 사용해야 합니다.

g_signal_new() 함수의 인자 중에서 중요한 항목만 설명하면, 첫번째 항목은 시그널 이름을 정의하고, 세번째 항목은 시그널 함수 포인터가 맨 나중에 실행될 지(G_SIGNAL_RUN_LAST), 또는 가장 먼저 실행될 지(G_SIGNAL_RUN_FIRST) 등을 지정합니다. 네번째 항목은 클래스 구조체에 정의된 시그널 함수 포인터 위치를 지정하고, 여덟번째는 시그널 콜백 함수의 리턴 형(type), 아홉번째는 콜백 함수에게 전달할 인자의 갯수, 열번째부터는 전달될 인자의 형(types)을 차례대로 정의합니다.

g_signal_new() 함수의 일곱번째 인자는 함수 호출시 인자를 처리하는 마샬링(marshalling) 함수를 지정하는데, 함수의 리턴 형(type)과 인자 목록, 인자의 각 형(type)이 정확히 일치되는 함수를 지정해야 합니다. 그런데 원하는 형태의 마샬링 함수를 GLib에서 기본으로 제공하지 않을 경우 glib-genmarshal 프로그램을 이용해 직접 C 소스 코드를 생성해서 사용해야 했는데, GLib 2.30 버전부터는 그냥 NULL을 지정하면 libffi 라이브러리를 이용해 구현한 g_cclosure_marshal_generic() 함수가 기본으로 호출되어, 알아서 자동으로 마샬링을 처리합니다.

정리하자면, GObject 시그널은 모델-뷰(model-view) 구조나 관찰자 패턴(observer pattern)을 구현하는데 사용하기도 하지만, 더 복잡한 객체 지향 시스템을 설계할 때도 유용합니다. 하지만, 여기서는 시그널의 특징과 개념만 설명하느라 전체 기능의 반의 반도 소개되지 않은 셈입니다. 따라서 더 깊은 이해와 활용을 원하시면 반드시 참고 매뉴얼을 한 번 정독하시길 권합니다.

그리고…

이 글에 사용된 모든 예제 소스를 실행해보고 싶으신 분은 직접 타이핑하지 마시고, 소스 코드를 다운로드한 뒤, 클러터(clutter) 개발 패키지 설치 후 make 명령으로 컴파일하면 됩니다.

그리고, 다른 프로그래머가 왜 C++, Java, Python 처럼 좋은 언어 놔두고 C 언어 기반에서 복잡한 GObject 같은 걸 가지고 객체 지향 프로그래밍을 할려고 애쓰냐고 물어본다면,  리눅스 커널 메일링 리스트 FAQ에 있는 유명한 다음 구절을 해석해서 미소지으며 알려주시기 바랍니다.

What’s important about object-oriented programming is the techniques, not the languages used.

뭐, 모든 도구는 필요한 곳이 반드시 있으니까 계속 존재합니다. 다만 내가 아직 그 쓰임새를 알지 못할 뿐이죠… :)

Dr. Memory 메모리 오류 / 누수 감지 도구

대부분의 작업을 리눅스 환경에서 진행하지만 가끔은 어쩔 수 없이 윈도우 프로그램을 디버깅합니다. 그런데 메모리 오류를 디버깅할 때 리눅스에서 애용하는 Valgrind 같은 괜찮은 무료(!) 도구가 없어서 아쉬었는데, 오늘 발견한 Dr. Memory 덕분에 매우 수월하게 메모리 오류 디버깅을 진행할 수 있었습니다. 또한, 리눅스에서도 사용할 수 있다고 하니 계속 디버깅에 사용해 볼 생각입니다.

사실, 이 포스팅은, 사용하기 쉬우면서도 Valgrind 만큼 강력한 Dr. Memory 덕분에 해묵은 버그와 오류까지 모조리 잡을 수 있게 되어, 그 고마움을 표시하고 싶어서 남기는 글입니다. 따라서, 사용법 같은 자세한 내용이 궁금하신 분은 홈페이지를 방문해 보시길… :)

모니터 없이 X 서버 실행 후 나중에 모니터 연결해도 화면이 안보인다면

제목이 조금 길지만, 이 글의 내용은 제목 그대로입니다. (참고로, 이 글은 최근 인텔 그래픽 칩셋을 대상으로 작성되었습니다. 즉, 다른 그래픽 칩셋 드라이버는 어떻게 동작하는지 확인을 안 해 보았다는 의미입니다)

요즘 X 서버는 연결된 모니터가 없고, /etc/X11/xorg.conf 파일에 수직/수평 주파수가 정의되어 있지 않다고 하더라도 일단 정상적으로 실행됩니다. 다만, 초기 해상도가 320×200 처럼 매우 작을 수 있습니다. 그런데 나중에 필요에 의해 모니터를 연결했는데 화면이 보이지 않는 경우가 발생합니다. 이 경우, 네트웍으로 접속해서 다음 명령어를 실행합니다.

$ export DISPLAY=:0.0
$ xrandr --auto

그러면 X 서버가 알아서 연결되어 있는 모니터를 출력으로 재설정하고 가장 선호하는(preferred) 해상도와 주파수를 선택합니다. 그런데 이를 자동으로 동작하게 하려면 udev 데몬의 도움을 받아야 합니다. 그래서 /etc/udev/rules.d 디렉토리에 확장자가 '.rules'인 파일을 생성하고 다음과 같이 내용을 채웁니다.

ACTION=="change", SUBSYSTEM=="drm", KERNEL=="card*", RUN+="/usr/bin/auto-xrandr.sh"

그리고 /usr/bin/auto-xrandr.sh 파일을 아래와 같이 작성한뒤 실행권한을 줍니다.

#!/bin/sh
[ "$DISPLAY" = "" ] && export DISPLAY=:0.0
xrandr --auto
xrandr --dpi 96

그러면 이제부터 모니터 연결시 자동으로 연결을 재설정하게 됩니다.

그런데, 만일 네트웍으로 접속했거나, udev 데몬에 의해 실행되는 경우 X 서버 인증이 안된 계정이라며 xrandr 명령이 실행이 거부됩니다. 이를 제대로 처리하려면 복잡한 인증 과정이 필요한데, 이를 쉽게 처리하려면 그냥 X 서버 시작할때 자동으로 실행되는 초기화 스크립트에 'xhost +' 명령어를 주면 인증을 무시하게 됩니다. 물론 보안상 좋은 방법은 아니지만, 폐쇠된 환경이라면 별로 문제가 없을 겁니다.

각각의 명령어에 대해 더 궁금하신 분은 관련 명령어 매뉴얼 페이지를 확인해 보시기 바랍니다.

참고로 조금만 더 설명하면, 노트북 외부 모니터 단자에 빔 프로젝트나 외부 모니터를 연결했을 경우도 xrandr 명령을 직접 사용하거나 지금까지 설명한 방법을 조금 다르게 응용할 수 있습니다.