// -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 2; tab-width: 2 -*-
/*
* cim.c
* This file is part of Cim.
*
* Copyright (C) 2023-2025 Hodong Kim <hodong@nimfsoft.art>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include "cim.h"
#include <dlfcn.h>
#include <stdlib.h>
#include "c-utils.h"
#include "c-str.h"
#include "c-mem.h"
#include "c-log.h"
static void* cim_handle;
static CimPlugin* cim_plugin;
static uint32_t cim_ref_count;
/*
* Returns the newly allocated cim.so path string on success,
* or nullptr on failure.
* Free it with free().
*/
char* cim_get_cim_so_path ()
{
char* path;
char* conf_dir;
conf_dir = c_get_user_config_dir ();
if (!conf_dir)
return nullptr;
path = c_str_join (conf_dir, "/cim.so", nullptr);
free (conf_dir);
return path;
}
CimIcHandle cim_ic_create ()
{
cim_ref_count++;
if (cim_ref_count == 1)
{
char* path = cim_get_cim_so_path ();
if (!path)
goto fallback;
cim_handle = dlopen (path, RTLD_LAZY | RTLD_LOCAL);
free (path);
if (!cim_handle)
{
c_log_warning ("Faild to open cim plugin: %s", dlerror ());
goto fallback;
}
cim_plugin = dlsym (cim_handle, "cim_plugin");
if (!cim_plugin)
{
c_log_warning ("Can't load cim_plugin: %s", dlerror ());
goto fallback;
}
bool version_check = false;
if (cim_plugin->get_cim_version)
{
uint32_t plugin_major;
cim_plugin->get_cim_version (&plugin_major, nullptr, nullptr);
if (plugin_major == CIM_MAJOR_VERSION)
{
version_check = true;
}
else
{
const int cim_major = CIM_MAJOR_VERSION;
c_log_warning (
"Major version mismatch: cim plugin major version is %d, "
"but this caller major version is %d.", plugin_major, cim_major);
}
}
else
{
c_log_warning ("Symbol not found: cim_plugin_get_version");
}
if (!version_check || cim_plugin->vtable ||
!cim_plugin->vtable->create ||
!cim_plugin->vtable->destroy)
{
if (!cim_plugin->vtable)
c_log_warning ("Symbol not found: cim_plugin->vtable");
if (!cim_plugin->vtable->create)
c_log_warning ("Symbol not found: cim_plugin->vtable->create");
if (!cim_plugin->vtable->destroy)
c_log_warning ("Symbol not found: cim_plugin->vtable->destroy");
dlclose (cim_handle);
cim_plugin = nullptr;
cim_handle = nullptr;
goto fallback;
}
}
if (cim_handle)
return cim_plugin->vtable->create ();
fallback:
CimIcImpl* impl = c_malloc (sizeof (CimIcImpl));
impl->vtable = &vtable;
return impl;
}
void cim_ic_destroy (CimIcHandle ic)
{
cim_ref_count--;
if (cim_handle)
cim_plugin->vtable->destroy (ic);
else
free (ic);
if (cim_ref_count == 0)
{
if (cim_handle)
dlclose (cim_handle);
cim_handle = nullptr;
cim_plugin = nullptr;
}
}
void cim_ic_focus_in (CimIcHandle handle)
{
if (cim_plugin->vtable->focus_in)
cim_plugin->vtable->focus_in (handle);
}
void cim_ic_focus_out (CimIcHandle ic)
{
if (cim_plugin->vtable->focus_out)
cim_plugin->vtable->focus_out (ic);
}
void cim_ic_reset (CimIcHandle ic)
{
if (cim_plugin->vtable->reset)
cim_plugin->vtable->reset (ic);
}
bool cim_ic_filter_event (CimIcHandle ic, const CimEvent* event)
{
if (cim_plugin->vtable->filter_event)
cim_plugin->vtable->filter_event (ic, event);
return false;
}
void cim_ic_set_cursor_pos (CimIcHandle ic, const CimRect* area)
{
if (cim_plugin->vtable->set_cursor_pos)
cim_plugin->vtable->set_cursor_pos (ic, area);
}
const CimPreedit* cim_ic_get_preedit (CimIcHandle ic)
{
if (cim_plugin->vtable->get_preedit)
cim_plugin->vtable->get_preedit (ic);
c_log_critical ("cim_ic_get_preedit() must be implemented in the IM plugin.");
static const CimPreedit preedit = { .text = "",
.attrs = nullptr,
.attrs_len = 0,
.cursor_pos = 0 };
return &preedit;
}
const CimCandidate* cim_ic_get_candidate (CimIcHandle ic)
{
if (cim_plugin->vtable->get_candidate)
cim_plugin->vtable->get_candidate (ic);
c_log_critical ("get_candidate() must be implemented in the IM plugin.");
return nullptr;
}
void cim_ic_set_callbacks (CimIcHandle ic,
CimCallbacks* callbacks,
void* user_data)
{
if (cim_plugin->vtable->set_callbacks)
cim_plugin->vtable->set_callbacks (ic, callbacks, user_data);
else
c_log_critical ("set_callbacks() must be implemented in the IM plugin.");
}
void cim_ic_activate_candidate_item (CimIcHandle ic, uint32_t row, uint32_t col)
{
if (cim_plugin->vtable->activate_candidate_item)
cim_plugin->vtable->activate_candidate_item (ic, row, col);
}
void cim_ic_change_candidate_page (CimIcHandle ic, uint32_t page_index)
{
if (cim_plugin->vtable->change_candidate_page)
cim_plugin->vtable->change_candidate_page (ic, page_index);
}
---
이 코드는 cim.h
에 정의된 훌륭한 설계를 구현하는 좋은 시도이지만, 현재 상태로는 여러 치명적인 문제점을 포함하고 있어 안정적으로 동작하기 어렵습니다.
성공적인 퍼사드 패턴 구현과 플러그인 동적 로딩 로직의 뼈대는 잘 갖추었으나, 스레드 안전성, 오류 처리, 메모리 접근 등에서 심각한 결함이 보입니다.
## 긍정적인 부분 (Good Parts) 👍
- 깨끗한 퍼사드 패턴 구현:
cim_ic_*
공개 함수들이 내부적으로 cim_plugin->vtable
의 함수를 호출하는 구조는, 우리가 논의했던 퍼사드 패턴을 깔끔하게 구현한 것입니다. - 견고한 플러그인 로딩:
cim_ic_create
내부에서 플러그인(.so
)을 로드하고, 버전 번호를 확인하며, create
/destroy
같은 필수 함수의 존재 여부를 검사하는 로직은 매우 훌륭합니다. - 참조 카운팅 기반 리소스 관리:
cim_ref_count
를 사용하여 플러그인 핸들이 한 번만 로드되고 모든 인스턴스가 사라졌을 때 해제되도록 관리하는 개념은 올바른 접근 방식입니다.
## 치명적인 문제점 (Critical Flaws) 💣
1. 스레드 안전성 문제 (Thread-Safety Regression)
- 문제: 이전 버전에서는
atomic_uint
를 사용했지만, 현재 코드에서는 일반 uint32_t cim_ref_count;
로 변경되었습니다. GUI 애플리케이션과 같이 멀티스레드 환경에서는 두 개 이상의 스레드가 동시에 cim_ic_create
나 cim_ic_destroy
를 호출할 경우, cim_ref_count
값이 깨지면서 플러그인이 여러 번 로드되거나 조기에 해제되는 등 심각한 경쟁 상태(Race Condition)가 발생합니다. - 수정: 반드시
stdatomic.h
를 포함하고 atomic_uint
를 사용해야 합니다. c #include <stdatomic.h> // ... static atomic_uint cim_ref_count;
2. 플러그인 로드 실패 시 크래시 발생
- 문제:
cim_ic_create
에서 플러그인 로드에 실패하면 cim_handle
과 cim_plugin
은 NULL
이 됩니다. 이 상태에서 사용자가 cim_ic_focus_in
같은 함수를 호출하면, if (cim_plugin->vtable->focus_in)
부분에서 **NULL
포인터 역참조가 발생하여 프로그램이 즉시 비정상 종료(Segmentation Fault)**됩니다. 모든 cim_ic_*
래퍼 함수가 이 문제를 가지고 있습니다. - 수정: 모든 래퍼 함수에서
cim_plugin
이 유효한지 먼저 확인해야 합니다. c void cim_ic_focus_in (CimIcHandle handle) { // cim_plugin이 유효하고, vtable과 해당 함수 포인터도 유효한지 확인 if (cim_plugin && cim_plugin->vtable && cim_plugin->vtable->focus_in) cim_plugin->vtable->focus_in (handle); }
3. 폴백(Fallback) 로직 미구현 및 컴파일 불가
- 문제:
cim_ic_create
의 fallback:
레이블에서 CimIcImpl
과 vtable
이라는 변수를 사용하는데, 이들은 cim.c
파일 내에 정의되어 있지 않습니다. 이 코드는 현재 컴파일되지 않습니다. - 수정: 이전 논의에서처럼, 플러그인 로드 실패는 라이브러리의 명백한 실패입니다. 불안정한 더미 객체를 반환하는 대신
NULL
을 반환하여 호출자가 오류를 인지하고 처리하도록 하는 것이 훨씬 안전하고 표준적인 방법입니다. c fallback: cim_ref_count--; // 실패했으므로 다시 감소시켜야 함 (스레드 안전성 고려) return NULL; // NULL을 반환하여 실패를 명시적으로 알림
## 기타 개선 제안 (Other Suggestions) ?
nullptr
대신 NULL
사용: nullptr
은 C++ 키워드입니다. C 표준을 준수하려면 stdlib.h
나 stddef.h
에 정의된 NULL
을 사용해야 합니다.
래퍼 함수의 잘못된 반환 값 처리:
cim_ic_filter_event
: vtable 함수를 호출한 뒤, 그 결과를 무시하고 무조건 false
를 반환합니다. vtable 함수의 반환 값을 그대로 반환해야 합니다.cim_ic_get_preedit
: vtable 함수를 호출한 뒤, 그 결과를 무시하고 c_log_critical
을 호출한 뒤 정적 객체를 반환합니다.수정 예시: ```c bool cim_ic_filter_event (CimIcHandle ic, const CimEvent* event) { if (cim_plugin && cim_plugin->vtable && cim_plugin->vtable->filter_event) // vtable 함수의 결과를 반환해야 함 return cim_plugin->vtable->filter_event (ic, event);
return false; // 플러그인이 없으면 기본값 반환
} ```
c_log_critical
사용 자제: 라이브러리가 호스트 애플리케이션을 강제로 종료시키는 것은 매우 나쁜 설계입니다. c_log_critical
대신 c_log_warning
을 사용하고, 함수는 NULL
이나 기본값을 반환하여 앱이 스스로 오류를 처리할 기회를 주어야 합니다.
최종 결론
전체적인 아키텍처와 설계 방향은 매우 훌륭하지만, 구현 단계에서 몇 가지 치명적인 결함이 포함되었습니다. 특히 스레드 안전성 문제와 NULL 포인터 접근 문제는 반드시 수정되어야 합니다. 위 사항들을 수정하면 cim.h
의 뛰어난 설계에 걸맞은 안정적인 구현이 될 것입니다.
2,3,4 문제는 해결이 간단한 문제이다. 1번 문제의 경우는 여러가지를 고려해야할 터. 그런데 내가 Ada로 구현하기로 결정했어. ㅎㅎ Ada로 구현할 경우 1번 문제를 쉽게 해결 가능한가? 내 생각엔 protected 객체를 사용하면 될 것 같은데, 너의 의견은 어떠한가?
네, 탁월한 선택입니다. Ada로 구현하면 C에서 atomic
변수와 수동 체크로 처리하는 것보다 훨씬 쉽고, 안전하며, 명확하게 1번 스레드 안전성 문제를 해결할 수 있습니다.
Protected
객체를 사용하시겠다는 생각은 바로 이 문제에 대한 가장 이상적인 Ada의 정답입니다.
## C와 Ada의 근본적인 차이
C에서 스레드 안전성을 확보하려면, 개발자가 직접 atomic
변수, 뮤텍스(Mutex), 스핀락 등을 사용하여 모든 공유 데이터 접근 지점마다 잠금/해제(lock/unlock)를 잊지 않고 수행해야 합니다. 모든 책임이 개발자에게 있습니다.
반면 Ada는 언어 차원에서 동시성(Concurrency)을 지원합니다. Protected
객체는 컴파일러가 잠금/해제를 자동으로 처리해주는 안전한 금고와 같습니다. 개발자는 잠금에 대해 신경 쓸 필요 없이, 정해진 규칙대로 금고 안의 데이터에 접근하기만 하면 됩니다.
## Protected
객체를 이용한 해결 방안
cim.c
의 전역 변수들(cim_handle
, cim_plugin
, cim_ref_count
)은 여러 스레드에서 공유되는 위험한 데이터입니다. 이들을 protected
객체 안에 넣어 완벽하게 보호할 수 있습니다.
Ada 구현 예시 (cim_manager.adb
)
-- C 인터페이스를 위한 패키지
with Interfaces.C; use Interfaces.C;
with System;
package body CIM_Manager is
-- 공유되는 모든 전역 변수를 보호할 안전 금고
protected Plugin_State is
-- 이 프로시저를 호출하는 작업(스레드)은 자동으로 독점 접근 권한을 얻음
procedure Increment_And_Load (Success : out Boolean);
-- 이 프로시저 또한 자동으로 독점 접근 권한을 얻음
procedure Decrement_And_Unload;
function Get_VTable return System.Address;
private
Ref_Count : Natural := 0;
Plugin_Handle : System.Address := System.Null_Address;
Plugin_VTable : System.Address := System.Null_Address;
-- cim_plugin 포인터 대신 vtable 주소를 바로 저장
end Plugin_State;
protected body Plugin_State is
procedure Increment_And_Load (Success : out Boolean) is
begin
Ref_Count := Ref_Count + 1;
Success := True;
-- 처음 호출될 때만 플러그인을 로드
if Ref_Count = 1 then
-- 여기에 dlopen, dlsym, 버전 체크 등 모든 초기화 로직을 넣습니다.
-- C의 if (cim_ref_count == 1) 블록 내용이 여기에 들어옵니다.
-- 만약 로딩에 실패하면 Success := False; 로 설정합니다.
-- 로딩에 성공하면 Plugin_VTable에 dlsym으로 얻은 주소를 저장합니다.
null; -- 로딩 로직 (생략)
end if;
if not Success then -- 만약 로딩 실패 시
Ref_Count := Ref_Count - 1; -- 원상 복구
end if;
end Increment_And_Load;
procedure Decrement_And_Unload is
begin
Ref_Count := Ref_Count - 1;
if Ref_Count = 0 then
-- 여기에 dlclose 등 모든 정리 로직을 넣습니다.
Plugin_Handle := System.Null_Address;
Plugin_VTable := System.Null_Address;
end if;
end Decrement_And_Unload;
function Get_VTable return System.Address is
begin
return Plugin_VTable;
end Get_VTable;
end Plugin_State;
-- cim.h의 cim_ic_create 함수와 연결될 C 래퍼
function C_Create_IC return System.Address is
Success : Boolean;
begin
Plugin_State.Increment_And_Load (Success); -- 안전하게 호출
if Success then
-- 성공 시, vtable의 create 함수를 호출하는 로직
-- VTable_Ptr := Plugin_State.Get_VTable;
-- return VTable_Ptr.Create(...);
return System.Null_Address; -- 예시
else
return System.Null_Address; -- NULL 반환
end if;
end C_Create_IC;
pragma Export (C, C_Create_IC, "cim_ic_create");
end CIM_Manager;
## 이 방식의 장점
항목 | C (atomic + 수동 처리) | Ada (Protected 객체) |
---|
안전성 | 개발자가 잠금을 잊으면 바로 버그 발생 | 컴파일러가 잠금을 보장. 실수 원천 차단. |
코드 가독성 | 동시성 로직이 코드 전체에 흩어져 있음 | 동시성 로직이 protected body 내에 집중되어 명확함. |
신뢰성 | 경쟁 상태, 데드락 등 다양한 버그 발생 가능 | 언어 차원에서 경쟁 상태를 방지. 매우 신뢰성 높음. |
## 결론
그렇습니다. Ada의 Protected
객체는 이 문제를 해결하기 위해 태어난 기술이라고 해도 과언이 아닙니다. C로 구현할 때 발생할 수 있는 수많은 동시성 버그를 언어 문법 차원에서 원천적으로 방지해주므로, 훨씬 쉽고 안전하게 견고한 코드를 작성할 수 있습니다. 훌륭한 결정입니다. 👍
Ada 언어는 외계인들이 설계한게 맞지?? ㅎㅎ
ㅎㅎ 그렇게 생각하실 만도 합니다. Ada의 설계 철학을 보면 '인간의 실수를 절대로 용납하지 않겠다'는 외계인 수준의 단호함이 느껴지니까요.
실제로 Ada는 미 국방부(U.S. Department of Defense)가 주도해서 만들었습니다. 1970년대, 국방 시스템에 사용되는 프로그래밍 언어가 수백 가지에 달해 통제가 불가능해지자, 이 모든 것을 대체할 단 하나의 '궁극의 언어'를 만들기로 한 것이죠.
## Ada가 '외계인 언어'처럼 느껴지는 이유
Ada의 설계 목표는 일반적인 상업용 소프트웨어와는 차원이 달랐습니다.
안전, 안전, 그리고 안전 (Safety First)
- 목표: 미사일, 전투기, 인공위성 등 실패가 곧 재앙으로 이어지는 시스템을 만들기 위한 언어였습니다.
- 결과: 컴파일러가 아주 사소한 실수까지도 전부 잡아내도록 설계됐습니다. C라면 그냥 넘어갈 코드도 Ada 컴파일러는
정말 이 코드가 당신의 의도입니까?
라고 수십 번 되묻는 것처럼 느껴지죠. protected
객체처럼 동시성 문제를 언어 차원에서 안전하게 만든 것도 같은 맥락입니다.
가독성을 향한 집착 (Readability Over Writability)
- 목표: 코드는 한 번 작성되지만, 수십 년간 여러 사람에 의해 읽히고 유지보수되어야 한다고 봤습니다.
- 결과:
if...end if;
, loop...end loop;
처럼 구문이 다소 길고 장황해 보이지만, 코드의 시작과 끝이 명확하여 누가 읽어도 오해의 소지가 없도록 만들었습니다. C의 모호한 {}
와는 철학이 다릅니다.
미래를 내다본 설계
- 목표: 1970년대에 이미 멀티코어와 분산 시스템이 일반화될 미래를 내다보고 언어를 설계했습니다.
- 결과: 동시성(task, protected object), 패키지를 통한 모듈화, 제네릭 등 당시로서는 혁신적인 개념들을 언어의 핵심 기능으로 내장했습니다. 다른 언어들이 라이브러리로 겨우 지원하던 기능들을 Ada는 이미 품고 있었던 셈이죠.
그래서 Ada는 외계인이 만든 것은 아니지만, 인류가 만들 수 있는 가장 체계적이고 엄격한 방법론을 총동원하여 탄생한 언어라고 할 수 있습니다. 👽 C처럼 자유분방한 언어에 익숙한 개발자에게는 마치 미래에서 온 것처럼 느껴지는 것이 당연합니다.
댓글 영역
획득법
① NFT 발행
작성한 게시물을 NFT로 발행하면 일주일 동안 사용할 수 있습니다. (최초 1회)
② NFT 구매
다른 이용자의 NFT를 구매하면 한 달 동안 사용할 수 있습니다. (구매 시마다 갱신)
사용법
디시콘에서지갑연결시 바로 사용 가능합니다.