пятница, 12 июня 2015 г.

Вызов внешних библиотечных функций из PL/SQL в Oracle 11gR2

Для того, чтобы в Oracle 11gR2 из PL/SQL вызвать функцию внешней динамической библиотеки необходимо:

  • создать динамически подгружаемую библиотеку с необходимой функцией, это .so - shared object - в ОС Unix и .dll - dynamic link library - в ОС Windows,
  • поместить динамическую библиотеку в директорию на сервере, где работает экземпляр Oracle,
  • сконфигурировать процесс exptproc для того, чтобы он мог найти и загрузить динамическую библиотеку,
  • создать в БД Oracle объект LIBRARY, который присвоит имя полному пути к динамической библиотеке,
  • создать в БД Oracle PL/SQL процедуру или функцию - обертку, указав объект LIBRARY и спецификацию вызова функции динамической библиотеки.

Я проделаю перечисленные шаги и продемонстрирую вызов функций внешней динамической библиотеки из PL/SQL.

Под пользователем oracle в ОС Linux создам директорию /home/oracle/test и в ней - файл с исходным текстом программы на C:

[oracle@tsuki test]$ cat hello.c
#include 

char result[4000];

char* hello(char* name) {
    sprintf(result, "Hello, %s!", name);
    return result;
}

void write(char* msg) {
    FILE* file;
    file = fopen("/home/oracle/test/hello.txt", "a");
    fprintf(file, "%s\n", msg);
    fclose(file);
}

Для функции hello предстоит сделать PL/SQL функцию-обертку, для функции write, не возвращающей значения, - PL/SQL процедуру-обертку.

Откомпилируем файл и построим динамическую библиотеку:

[oracle@tsuki test]$ gcc -shared -o libhello.so -fPIC hello.c
[oracle@tsuki test]$ ls -l
-rw-r--r-- 1 oracle oinstall  276 Jun  7 12:46 hello.c
-rwxr-xr-x 1 oracle oinstall 6545 Jun  7 12:46 libhello.so

Подробнее о том, как создать динамическую библиотеку в Linux при помощи инструментария gcc, можно прочитать в статье Shared libraries with GCC on Linux.

Прежде чем пытаться вызывать функции библиотеки из PL/SQL, проверю их работоспособность с помощью программы на C. Для этого, создам заголовочный файл для библиотеки libhello.so и тестовую программку test.c.

[oracle@tsuki test]$ cat hello.h
#ifndef hello_h_
#define hello_h_

extern char* hello(char* name);
extern void write(char* msg);

#endif // hello_h_

[oracle@tsuki test]$ cat test.c
#include 
#include "hello.h"

int main(void) {
    write(hello("world"));
    return 0;
}

Создам исполняемый файл test:

[oracle@tsuki test]$ gcc -Wall -L/home/oracle/test -o test test.c -lhello
[oracle@tsuki test]$ ls -l
-rw-r--r-- 1 oracle oinstall  276 Jun  7 12:46 hello.c
-rw-r--r-- 1 oracle oinstall  117 Jun  7 12:57 hello.h
-rwxr-xr-x 1 oracle oinstall 6545 Jun  7 12:46 libhello.so
-rwxr-xr-x 1 oracle oinstall 7072 Jun  7 13:01 test
-rw-r--r-- 1 oracle oinstall   99 Jun  7 12:59 test.c

Установлю переменную LD_LIBRARY_PATH (для того, чтобы ОС нашла и подгрузила библиотеку libhello.so) и выполню test:

[oracle@tsuki test]$ ./test
./test: error while loading shared libraries: libhello.so: cannot open shared object file: No such file or directory

[oracle@tsuki test]$ export LD_LIBRARY_PATH=/home/oracle/test:$LD_LIBRARY_PATH

[oracle@tsuki test]$ ./test

[oracle@tsuki test]$ ls -l hello.txt
-rw-r--r-- 1 oracle oinstall   14 Jun  7 13:05 hello.txt

[oracle@tsuki test]$ cat hello.txt
Hello, world!

[oracle@tsuki test]$ ./test

[oracle@tsuki test]$ cat hello.txt
Hello, world!
Hello, world!

Убедившись, что библиотека libhello.so работоспособна, перехожу к конфигурированию процесса extproc. Это процесс Oracle, который загружает внешние библиотеки при вызове функций этих библиотек из PL/SQL.

До версии Oracle 11gR2 для запуска процесса extproc он должен был быть явно сконфигурирован в файлах listener.ora и tnsnames.ora. Однако, начиная с версии Oracle 11gR2 процесс extproc не нуждается в явном конфигурировании, и изменять указанные файлы больше не требуется.

В файле extproc.ora (расположенном в директории $ORACLE_HOME/hs/admin в ОС Unix и в %ORACLE_HOME%\hs\admin в ОС Windows) определяются переменные окружения для процесса extproc. Приведу содержание этого файла с описанием переменной EXTPROC_DLLS и ее установкой для работы с библиотекой libhello.so:

[oracle@tsuki ~]$ cat $ORACLE_HOME/hs/admin/extproc.ora
#
# extproc.ora is used by extproc in the default Oracle configuration.
#
# This is a sample extproc init file that contains a name-value(s) pair which
# is same as the value of ENVS parameter in listener.ora file.
#
# Syntax: SET name=value (environment variable name and value)
#
# When specifying the EXTPROC_DLLS environment variable to restrict the DLLs
# that extproc is allowed to load, you may set EXTPROC_DLLS to one of the
# following values:
#
# * ONLY (maximum security)
#
#   When EXTPROC_DLLS=ONLY:DLL[:DLL], only the specified DLL(s) can be loaded.
#
#   Syntax: SET EXTPROC_DLLS=ONLY:DLL[:DLL]
#
# * NULL (the default value)
#
#   When EXTPROC_DLLS=, only the DLL(s) in $ORACLE_HOME/bin and ORACLE_HOME/lib
#   can be loaded.
#
#   Syntax: SET EXTPROC_DLLS=
#
# * Colon-seperated list of the DLLs
#
#   When EXTPROC_DLLS=DLL[:DLL], the specified DLL(s) and the DLLs in
#   $ORACLE_HOME/bin and ORACLE_HOME/lib can be loaded.
#
#   Syntax: SET EXTPROC_DLLS=DLL:DLL
#
# * ANY
#   When EXTPROC_DLLS=ANY, DLL checking is disabled.
#
#   Syntax: SET EXTPROC_DLLS=ANY
#
SET EXTPROC_DLLS=ONLY:/home/oracle/test/libhello.so

Следующий шаг - создать объект LIBRARY в БД Oracle:

SQL> create or replace library lib_hello as '/home/oracle/test/libhello.so';
Library created

Осталось создать PL/SQL функцию-обертку для библиотечной функции hello и процедуру-обертку для библиотечной функции write. Нам понадобится передавать внешним функциям строковые параметры и получать строкоый результат от функции hello.

Создам PL/SQL функцию и процедуру для вызова внешних функций в пакете hello:

SQL> create or replace package hello as
  2  
  3     function hello(name varchar2) return varchar2
  4     as language c
  5     library lib_hello
  6     name "hello"
  7     ;
  8  
  9     procedure write(msg varchar2)
 10     as language c
 11     library lib_hello
 12     name "write"
 13     parameters (msg string)
 14     ;
 15  end hello;
 16  /
Package created

SQL> show error
No errors for PACKAGE AY.HELLO

Здесь для функции hello не специфицированы типы данных параметра и возвращаемого значения - это значит, Oracle выполнит неявные преобразования к соответствующим типам данных по умолчанию. Типу данных PL/SQL VARCHAR2 соответствует по умолчанию тип спецификации STRING (а ему - тип C char*). А для процедуры write тип данных параметра msg специфицирован явно - чтобы продемонстрировать, как это делается.

Вызову функцию hello и процедуру write:

SQL> select hello.hello('мир') from dual;
HELLO.HELLO('МИР')
--------------------------------------------------------------------------------
Hello, мир!

SQL> call hello.write(hello.hello('everybody'));
Method called

Вернусь на сервер, в директорию /home/oracle/test и проверю содержимое файла hello.txt:

[oracle@tsuki test]$ cat hello.txt
Hello, world!
Hello, world!
Hello, everybody!

Итак, мы получили ожидаемый результат.

Ниже я приведу соответствия основных типов данных PL/SQL, типов данных для спецификации внешних процедур и типов данных С. Таблица подготовлена по данным Advanced Application Developer's Guide из официальной документации Oracle 11gR2:

PL/SQLТип данных спецификацииТип данных C
BINARY_INTEGER
BOOLEAN
PLS_INTEGER
[UNSIGNED] CHAR
[UNSIGNED] SHORT
[UNSIGNED] INT
[UNSIGNED] LONG
[unsigned] char [*]
[unsigned] short [*]
[unsigned] int [*]
[unsigned] long [*]
FLOAT
REAL
FLOAT float [*]
DOUBLE PRECISION DOUBLE double [*]
NUMBER OCINUMBER OCINumber *
CHAR
CHARACTER
LONG
NCHAR
NVARCHAR2
ROWID
VARCHAR
VARCHAR2
STRING
OCISTRING
char *
OCIString *
LONG RAW
RAW
RAW
OCIRAW
unsigned char *
OCIRaw *
BFILE
BLOB
CLOB
NCLOB
OCILOBLOCATOR OCILobLocator *
DATE OCIDATE OCIDate *
TIMESTAMP
TIMESTAMP WITH TIME ZONE
TIMESTAMP WITH LOCAL TIME ZONE
OCIDateTime OCIDateTime [*]
INTERVAL DAY TO SECOND
INTERVAL YEAR TO MONTH
OCIInterval OCIInterval [*]
composite object types: collections (varrays, nested tables) OCICOLL OCIColl * or OCIArray * or OCITable *

При передаче значений из PL/SQL во внешние C функции и обратно возникают, в частности, следующие вопросы:

  • как передать значение NULL (то есть, отсутствие значения)?
  • как передать ограничения на размер данных (например, результирующая строка не должна быть длинее 100 символов)?
  • как передать кодировку, в которой представлены строковые данные?

Разработчики Oracle позаботились дать ответы на эти вопросы с помощью параметров-свойств (properties), указываемых в части PARAMETERS спецификации вызова наравне с обычными параметрами. В частности, определены следующие свойства:

INDICATOR
показывает, что другой параметр или возвращаемый результат имеет значение NULL.
LENGTH
содержит текущую длину другого IN или [IN] OUT параметра или возвращаемого значения.
MAXLEN
содержит максимально допустимую длину другуого [IN] OUT параметра или возвращаемого значения.
CHARSETID
содержит идентификатор набора символов передаваемой из PL/SQL строки (тот самый, что возвращает SQL функция NLS_CHARSET_ID).
CHARSETFORM
содержит 1 для набора символов БД (database charset, используемый с типами данных CHAR, VARCHAR2, CLOB) и 2 для национального набора символов (national charset, используемый с типами данных NCHAR, NVARCHAR2, NCLOB).

Подробнее о спецификации вызова внешних процедур и о работе с параметрами-свойствами можно прочитать в Database Advanced Application Developer's Guide.

В заключение, удалю из базы данных следы экспериментов:

SQL> drop package hello;
Package dropped

2 комментария: