런타임 로드 가능한 확장
런타임 로드 가능한 확장 (Run-Time Loadable Extensions)
1. 개요 (Overview)
SQLite는 런타임에 확장(새로운 애플리케이션 정의 SQL 함수 (application-defined SQL functions), 정렬 시퀀스 (collating sequences), 가상 테이블 (virtual tables), VFS 포함)을 로드하는 기능이 있어요. 이 기능을 사용하면 확장 코드를 애플리케이션과 별도로 개발하고 테스트한 다음 필요에 따라 로드할 수 있어요.
본문
확장은 애플리케이션에 정적으로 링크될 수도 있어요. 아래에 보이는 코드 템플릿은 런타임 로드 가능한 확장처럼 정적으로 링크된 확장에서도 똑같이 잘 작동해요. 단, 엔트리 포인트 함수("sqlite3_extension_init")에 다른 이름을 주어 애플리케이션이 두 개 이상의 확장을 포함할 때 이름 충돌을 피해야 해요.
2. 확장 로드하기 (Loading An Extension)
SQLite 확장은 공유 라이브러리 또는 DLL이에요. 그것을 로드하려면 SQLite에 공유 라이브러리 또는 DLL을 포함하는 파일의 이름과 확장을 초기화하는 엔트리 포인트를 제공해야 해요. C 코드에서 이 정보는 sqlite3_load_extension() API를 사용해 제공돼요. 추가 정보는 그 루틴에 대한 문서를 참조하세요.
운영체제마다 공유 라이브러리에 다른 파일 이름 접미사를 사용한다는 점에 유의하세요. Windows는 ".dll", Mac은 ".dylib", Mac이 아닌 대부분의 unix는 ".so"를 사용해요. 코드를 이식 가능하게 만들고 싶다면 공유 라이브러리 파일 이름에서 접미사를 생략할 수 있으며, sqlite3_load_extension() 인터페이스가 적절한 접미사를 자동으로 추가해요.
확장을 로드하는 데 사용할 수 있는 SQL 함수도 있어요: load_extension(X,Y). 이것은 sqlite3_load_extension() C 인터페이스와 똑같이 작동해요.
확장을 로드하는 두 방법 모두 확장의 엔트리 포인트 이름을 지정할 수 있게 해줘요. 이 인자를 비워 둘 수 있어요 - sqlite3_load_extension() C 언어 인터페이스에 NULL 포인터를 전달하거나 load_extension() SQL 인터페이스의 두 번째 인자를 생략하면 - 확장 로더 로직이 스스로 엔트리 포인트를 알아내려고 시도할 거예요. 먼저 일반적인 확장 이름 "sqlite3_extension_init"을 시도할 거예요. 그것이 작동하지 않으면 "sqlite3_X_init" 템플릿을 사용해 엔트리 포인트를 구성하는데, 여기서 X는 마지막 "/" 이후와 첫 번째 뒤따르는 "." 이전의 파일 이름에 있는 모든 ASCII 문자의 소문자 등가물로 대체되고, 처음 세 문자가 "lib"이면 생략돼요. 예를 들어 파일 이름이 "/usr/lib/libmathfunc-4.8.so"이면 엔트리 포인트 이름은 "sqlite3_mathfunc_init"이 돼요. 또는 파일 이름이 "./SpellFixExt.dll"이면 엔트리 포인트는 "sqlite3_spellfixext_init"으로 불릴 거예요.
보안상의 이유로 확장 로딩은 기본적으로 꺼져 있어요. C 언어 또는 SQL 확장 로딩 함수를 사용하려면 먼저 애플리케이션에서 sqlite3_db_config(db,SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION,1,NULL) C 언어 API를 사용해 확장 로딩을 활성화해야 해요.
명령줄 셸 (command-line shell)에서 확장은 ".load" 도트 명령을 사용해 로드할 수 있어요. 예를 들어:
.load ./YourCode
명령줄 셸 프로그램은 이미 여러분을 위해 확장 로딩을 활성화해 두었으므로(설정의 일부로 sqlite3_enable_load_extension() 인터페이스를 호출함) 위 명령은 특별한 스위치, 설정 또는 다른 복잡함 없이 작동해요.
인자 하나를 가진 ".load" 명령은 zProc 매개변수를 NULL로 설정한 상태로 sqlite3_load_extension()을 호출하여, SQLite가 먼저 "sqlite3_extension_init"이라는 엔트리 포인트를 찾고 그 다음 "X"가 파일 이름에서 파생된 "sqlite3_X_init"을 찾게 해요. 확장에 다른 이름의 엔트리 포인트가 있다면, 그 이름을 두 번째 인자로 제공하면 돼요. 예를 들어:
.load ./YourCode nonstandard_entry_point
3. 로드 가능한 확장 컴파일하기 (Compiling A Loadable Extension)
로드 가능한 확장은 C 코드예요. 대부분의 unix 계열 운영체제에서 컴파일하려면 보통 명령은 다음과 같아요:
gcc -g -fPIC -shared YourCode.c -o YourCode.so
Mac은 unix 계열이지만 일반적인 공유 라이브러리 규칙을 따르지 않아요. Mac에서 공유 라이브러리를 컴파일하려면 다음과 같은 명령을 사용해요:
gcc -g -fPIC -dynamiclib YourCode.c -o YourCode.dylib
라이브러리를 로드하려고 할 때 "mach-o, but wrong architecture"라고 말하는 오류 메시지를 받으면, 애플리케이션이 어떻게 빌드되는지에 따라 gcc에 "-arch i386" 또는 "arch x86_64" 명령줄 옵션을 추가해야 할 수도 있어요.
MSVC를 사용해 Windows에서 컴파일하려면 다음과 유사한 명령이 보통 작동해요:
cl YourCode.c -link -dll -out:YourCode.dll
MinGW를 사용해 Windows용으로 컴파일하려면 명령줄은 unix와 같지만 출력 파일 접미사가 ".dll"로 바뀌고 -fPIC 인자가 생략돼요:
gcc -g -shared YourCode.c -o YourCode.dll
4. 로드 가능한 확장 프로그래밍 (Programming Loadable Extensions)
템플릿 로드 가능한 확장은 다음 세 가지 요소를 포함해요:
- 소스 코드 파일의 맨 위에 "
#include <sqlite3.h>" 대신 "#include <sqlite3ext.h>"를 사용해요. - "
#include <sqlite3ext.h>" 줄 바로 뒤에 매크로 "SQLITE_EXTENSION_INIT1"을 한 줄에 놓아요. - 대략 다음과 같은 확장 로딩 엔트리 포인트 루틴을 추가해요:
#ifdef _WIN32
__declspec(dllexport)
#endif
int sqlite3_extension_init( /* <== Change this name, maybe */
sqlite3 *db,
char **pzErrMsg,
const sqlite3_api_routines *pApi
){
int rc = SQLITE_OK;
SQLITE_EXTENSION_INIT2(pApi);
/* insert code to initialize your extension here */
return rc;
}
일반적인 "sqlite3_extension_init" 이름을 사용하는 대신 생성할 공유 라이브러리 이름에 대응하도록 엔트리 포인트 이름을 맞춤화하는 것이 좋아요. 확장에 사용자 지정 엔트리 포인트 이름을 주면, 나중에 런타임 링크 대신 정적 링크를 사용하기로 결정할 때 링커 충돌 없이 두 개 이상의 확장을 같은 프로그램에 정적으로 링크할 수 있게 해줘요. 공유 라이브러리가 위 컴파일러 예제에서처럼 "YourCode.so" 또는 "YourCode.dll" 또는 "YourCode.dylib"로 명명된다면, 올바른 엔트리 포인트 이름은 "sqlite3_yourcode_init"이 돼요.
다음은 시작하기 위해 복사/붙여넣기할 수 있는 완전한 템플릿 확장이에요:
/* Add your header comment here */
#include <sqlite3ext.h> /* Do not use <sqlite3.h>! */
SQLITE_EXTENSION_INIT1
/* Insert your extension code here */
#ifdef _WIN32
__declspec(dllexport)
#endif
/* TODO: Change the entry point name so that "extension" is replaced by
** text derived from the shared library filename as follows: Copy every
** ASCII alphabetic character from the filename after the last "/" through
** the next following ".", converting each character to lowercase, and
** discarding the first three characters if they are "lib".
*/
int sqlite3_extension_init(
sqlite3 *db,
char **pzErrMsg,
const sqlite3_api_routines *pApi
){
int rc = SQLITE_OK;
SQLITE_EXTENSION_INIT2(pApi);
/* Insert here calls to
** sqlite3_create_function_v2(),
** sqlite3_create_collation_v2(),
** sqlite3_create_module_v2(), and/or
** sqlite3_vfs_register()
** to register the new features that your extension adds.
*/
return rc;
}
4.1. 예제 확장 (Example Extensions)
완전하고 작동하는 로드 가능한 확장의 많은 예는 SQLite 소스 트리의 ext/misc 하위 디렉토리에서 볼 수 있어요. 그 디렉토리의 각 파일은 별도의 확장이에요. 문서는 파일의 헤더 주석으로 제공돼요. ext/misc 하위 디렉토리의 몇 가지 확장에 대한 간단한 메모는 다음과 같아요:
- carray.c — carray 테이블 값 함수 (carray table-valued function)의 구현.
- compress.c — 텍스트 또는 blob 내용의 zLib 압축을 수행하는 애플리케이션 정의 SQL 함수 (application-defined SQL functions) compress()와 uncompress()의 구현.
- rot13.c — rot13() SQL 함수의 구현. 확장 함수의 아주 단순한 예이며 새 확장을 만들 때 템플릿으로 유용해요.
- series.c — generate_series 가상 테이블 (virtual table)과 테이블 값 함수 (table-valued function)의 구현. 새 가상 테이블을 작성하기 위한 템플릿으로 사용될 수 있는 비교적 단순한 가상 테이블 구현 예시예요.
그 외 더 복잡한 확장들은 ext/misc/ 이외의 ext/ 아래 하위 폴더에서 찾을 수 있어요.
5. 영구적인 로드 가능한 확장 (Persistent Loadable Extensions)
로드 가능한 확장의 기본 동작은 원래 sqlite3_load_extension()을 호출한 데이터베이스 연결이 닫힐 때 프로세스 메모리에서 언로드되는 것이에요. (다시 말해, sqlite3_vfs 객체의 xDlClose 메서드가 데이터베이스 연결이 닫힐 때 모든 확장에 대해 호출돼요.) 하지만 초기화 절차가 SQLITE_OK 대신 SQLITE_OK_LOAD_PERMANENTLY를 반환하면, 확장은 언로드되지 않고(xDlClose가 호출되지 않음) 프로세스 메모리에 무기한 남아 있게 돼요. SQLITE_OK_LOAD_PERMANENTLY 반환 값은 새 VFS를 등록하려는 확장에 유용해요.
정리하면: 초기화 함수가 SQLITE_OK_LOAD_PERMANENTLY를 반환하는 확장은 데이터베이스 연결이 닫힌 후에도 메모리에 계속 존재해요. 하지만 확장은 이후의 데이터베이스 연결에 자동으로 등록되지는 않아요. 이로 인해 새 VFS를 구현하는 확장을 로드할 수 있게 돼요. 새 SQL 함수, 정렬 시퀀스 및/또는 가상 테이블을 구현하는 확장을 영구적으로 로드하고 등록하여 그 추가된 기능이 이후의 모든 데이터베이스 연결에 제공되게 하려면, 초기화 루틴이 그 서비스를 등록할 하위 함수에 sqlite3_auto_extension()을 호출해야 해요.
vfsstat.c 확장은 새 VFS와 새 가상 테이블을 모두 영구적으로 등록하는 로드 가능한 확장의 예를 보여줘요. 그 확장의 sqlite3_vfsstat_init() 초기화 루틴은 확장이 처음 로드될 때 한 번만 호출돼요. 새 "vfslog" VFS를 그 한 번만 등록하고, "vfslog" VFS를 구현하는 데 사용된 코드가 메모리에 남아 있도록 SQLITE_OK_LOAD_PERMANENTLY를 반환해요. 초기화 루틴은 또한 "vstatRegister()" 함수에 대한 포인터에 sqlite3_auto_extension()을 호출하여 이후의 모든 데이터베이스 연결이 시작할 때 "vstatRegister()" 함수를 호출하고, 따라서 "vfsstat" 가상 테이블을 등록하게 해요.
6. 런타임 로드 가능한 확장 정적으로 링크하기 (Statically Linking A Run-Time Loadable Extension)
정확히 같은 소스 코드가 런타임 로드 가능한 공유 라이브러리 또는 DLL과 애플리케이션에 정적으로 링크된 모듈 양쪽에 사용될 수 있어요. 이는 유연성을 제공하고 같은 코드를 다른 방식으로 재사용할 수 있게 해줘요.
확장을 정적으로 링크하려면 -DSQLITE_CORE 컴파일 타임 옵션을 추가하기만 하면 돼요. SQLITE_CORE 매크로는 SQLITE_EXTENSION_INIT1과 SQLITE_EXTENSION_INIT2 매크로가 no-op이 되게 해요. 그런 다음 세 번째 "pApi" 매개변수에 NULL 포인터를 전달하면서 엔트리 포인트를 직접 호출하도록 애플리케이션을 수정해요.
두 개 이상의 확장을 정적으로 링크할 예정이라면 일반적인 "sqlite3_extension_init" 엔트리 포인트 이름 대신 확장 파일 이름에 기반한 엔트리 포인트 이름을 사용하는 것이 특히 중요해요. 일반 이름을 사용하면 같은 심볼의 정의가 여러 개가 되어 링크가 실패할 거예요.
애플리케이션에서 여러 데이터베이스 연결을 열 경우, 각 데이터베이스 연결에 대해 확장 엔트리 포인트를 개별적으로 호출하는 대신 sqlite3_auto_extension() 인터페이스를 사용해 확장을 등록하고 각 데이터베이스 연결이 열릴 때 자동으로 시작되게 하는 것을 고려해 볼 수 있어요. 각 확장을 한 번만 등록하면 되며, main() 루틴의 시작 부분 부근에서 그렇게 할 수 있어요. sqlite3_auto_extension() 인터페이스를 사용해 확장을 등록하면 확장이 코어 SQLite에 내장된 것처럼 작동해요 - 새 데이터베이스 연결을 열 때마다 초기화할 필요 없이 자동으로 존재해요. 확장을 등록하기 전에 sqlite3_config()를 사용해 구성할 필요한 모든 구성을 완료해야 한다는 점을 잊지 마세요. sqlite3_auto_extension() 인터페이스가 암시적으로 sqlite3_initialize()를 호출하기 때문이에요.
7. 구현 세부 사항 (Implementation Details)
SQLite는 sqlite3_vfs 객체의 xDlOpen(), xDlError(), xDlSym(), xDlClose() 메서드를 사용해 런타임 확장 로딩을 구현해요. 이 메서드들은 unix에서 dlopen() 라이브러리를 사용해(이것이 SQLite가 unix 시스템에서 보통 "-ldl" 라이브러리에 링크되어야 하는 이유를 설명해요) 그리고 Windows에서 LoadLibrary() API를 사용해 구현돼요. 비정상적인 시스템을 위한 사용자 지정 VFS에서는 이 메서드들을 모두 생략할 수 있는데, 이 경우 런타임 확장 로딩 메커니즘은 작동하지 않아요 (단, 엔트리 포인터가 고유하게 명명된다면 확장 코드를 여전히 정적으로 링크할 수 있어요). SQLite는 SQLITE_OMIT_LOAD_EXTENSION으로 컴파일하여 빌드에서 확장 로딩 코드를 생략할 수 있어요.