Chapter 18

Overview

In the beginning, PL/SQL, and only PL/SQL, was used as a language with which to program the Oracle server, and Oracle client applications, within the server itself. Starting with version 8.0 of Oracle, the ability to implement stored procedures in other languages was introduced. This feature is called external procedures and covers both C (or anything that is C callable) and Java-based stored procedures. In this chapter, we will focus exclusively on C; the next chapter covers Java.

This chapter will cover external procedures from an architectural perspective, showing you how they have been implemented by the Oracle kernel developers. Additionally, we will see how you must configure your server to support them, and how you should configure your server for safety reasons. We will demonstrate how to write an external procedure using Oracle's Pro*C. This external procedure will be used to write the contents of any LOB to the server's file system. Before we get to that example though, we will work through a 'primer' example that demonstrates how to send and receive all interesting datatypes to and from PL/SQL and C. This primer will also develop a generic template useful for developing all subsequent C based external procedures quickly. We will cover how the C code should be implemented, with a mind towards being able to support this server-side code. We will also look at the implementation of the SQL binding we create for this external procedure. We will learn how we make it callable from SQL and PL/SQL, and how we should code the binding layer for ease of use on part of the people who will actually use our developed code. Lastly, we will cover the pros and cons of external procedures, and cover any miscellaneous server errors (ORA-XXXX errors) you might encounter.

The Pro*C example that will be developed in this chapter supplies a missing piece of server functionality. Oracle supplies the package DBMS_LOB to manipulate large objects (LOB). Within this package is a procedure, loadfromfile, which reads an arbitrary file from the OS file system, into the database and stores it there. However, they do not supply a function, writetofile, to take the contents of a LOB and write it to the OS - a common requirement. We will rectify this issue, and create for ourselves a LOB_IO package. This package allows us to write any BLOB, CLOB, or BFILE object to a separate file outside of the database (so for BFILEs we have effectively given ourselves a copy command since the source BFILE is already located outside of the database).

When Are They Used?

A single language or environment is not capable of providing every single feature and function you could ever want. Every language has shortcomings, things it cannot do, which you really want to do - there are features missing, things overlooked. When I'm writing in C, sometimes Assembler comes in handy. When I'm writing in Java, sometimes PL/SQL comes in handy. The point being that you don't always drop down to a 'lower level' language - sometimes you take a step up to a higher level. External procedures would be considered a 'step down' to a lower level language. You will typically use them to integrate existing C callable code you already have (a DLL - Dynamic Link Library on Windows; supplied by some third party you would like to call from the database), or to extend the functionality of some existing packages, as we are. This is the same technology Oracle itself uses to extend the server's capabilities - for example we saw how interMedia makes use of this feature in the previous chapter, and in Chapter 13 on Materialized Views we saw how DBMS_OLAP makes use of this capability.

The first external procedure I wrote was a 'simple TCP/IP' client. With it, in version 8.0.3 of the database, I gave PL/SQL the ability to open a TCP/IP socket to an existing server and send and receive messages. The server I could connect to could have been a Net News Transport Protocol (NNTP) server, an Internet Message Access Protocol (IMAP) server, a Simple Mail Transfer Protocol (SMTP) server, a Post Office Protocol (POP) server, a web server, or so on. By 'teaching' PL/SQL how to use a socket, I opened a whole new spectrum of opportunities. I could now:

  • Send e-mail from a trigger using SMTP

  • Incorporate e-mail into the database using POP

  • Index a newsgroup through interMedia text using NNTP

  • Access virtually any network based service available to me

Instead of thinking of the server as a server, I started thinking of it as a client - a client of all of these other servers. Once I got their data into my database, I could do a lot of things with it (index it, search it, serve it up in a different way, and so on).

Over time, this became such a frequently used tool that it is now an integrated feature of the database. Starting with release 8.1.6 of the Oracle server, all of the services that had been provided in the simple TCP/IP client are now implemented in the UTL_TCP package.

Since that time, I have written a couple of other external procedures. Some to get the system clock down to a finer resolution than the built-in SYSDATE function would return, others to execute operating system commands, or get the system's timezone, or to list the contents of a directory. The most recent one that I have developed, and the one that we will explore here, is a function to take the contents of any LOB, be it a Character LOB (CLOB), Binary LOB (BLOB), or BFILE, and write it to a named file. Another nice side effect of this package will be functionality similar to UTL_FILE, but for binary files (which UTL_FILE cannot generate). Since the server supports the concept of a Temporary LOB, and has functions to WRITE to a Temporary LOB, this new package we are going to implement will give us the ability to write an arbitrary binary file from PL/SQL. So in short, what we will get from this package is:

  • The capability to export any LOB to an external file on the server.

  • The capability to write any binary file of up to virtually any size with any data (this is similar to UTL_FILE which works with text data, but fails with arbitrary binary data).

As you can see by some of the examples above, the reasons for using an external procedure can be many and varied. Typically, you are using it to:

  • Supply missing functionality.

  • Integrate existing code, perhaps from a legacy system that performs data validation.

  • Speed up your processing. Compiled C will be able to perform some computationally expensive operation faster than interpreted PL/SQL or Java.

As always, the choice to use something like external procedures comes with certain cost. There is the cost of developing the code in C, which is more complex , in my opinion, than developing in PL/SQL. There is the cost of portability - or the potential inability to be portable. If you develop a DLL on Windows, there is no guarantee that source code you wrote would function on a UNIX machine, or vice versa. I'm of the opinion that you should only use an external procedure when the language (PL/SQL) gives you no other opportunity.

How Are They Implemented?

External procedures run in a process physically separate from the database. This is for reasons of safety. While it would be technically possible for the existing database server processes to dynamically load your DLL (on Windows) or .so (Shared Object code on Solaris) at run-time, it would expose the server to unnecessary risk. Your code would have access to the same memory space as the server processes do, and this might include areas of memory such as the Oracle SGA. This could allow developed code to accidentally corrupt, or otherwise damage, kernel data structures, possibly leading to loss of data, or a crashed database instance. In order to avoid this, external processes are executed as a separate process that does not share those memory areas with the server.

In most cases, the separate process would be configured to execute as a user other than the Oracle software account. The reason for this is much the same as why they are run in a separate process - safety. For example, we are going to create an external procedure that is capable of writing files to disk (as the example we will develop below does). Let's say you are on UNIX, and the external procedure is executing as the Oracle software owner. Someone calls your new function and asks to write a BLOB to /d01/oracle/data/system.dbf. Since the Oracle software owner is the user ID executing this code, we will be able to do this, thus inadvertently overwriting our system tablespace with the contents of some BLOB. We might not even notice that this happened until we shutdown and restarted our database (many days later). If we had run the external procedure as some less privileged user, this could not happen (that user would not have WRITE on the system.dbf file). So, for this reason, when we get to the section on configuring the server for external procedures, we'll find out how to set up a 'safe' EXTPROC (EXTernal PROCedure) listener that runs under a different OS account. This is very similar to why web servers typically execute as the user nobody on UNIX, or in some low-privileged account in Windows.

So, when you invoke an external procedure, Oracle will create an OS process called EXTPROC for you. It does this by contacting the Net8 listener. The Net8 listener will create the EXTPROC process for us in much the same way as it spawns dedicated servers or shared servers. This can be seen in Windows NT by using the NT Resource Toolkit utility tlist to print a tree of processes and subprocesses. For example, I started a session that accessed an external procedure, and then issued tlist -t, and I see the following:

C:\bin>tlist -t System Process (0) System (8)   smss.exe (140)     csrss.exe (164)     winlogon.exe (160)       services.exe (212)         svchost.exe (384)         SPOOLSV.EXE (412)         svchost.exe (444)         regsvc.exe (512)         stisvc.exe (600)         ORACLE.EXE (1024)           ORADIM.EXE (1264)         TNSLSNR.EXE (1188)           EXTPROC.EXE (972)       lsass.exe (224)       ... 

This shows that the TNSLSNR.EXE process is the parent process of the EXTPROC.EXE. The EXTPROC process and your server process can now communicate. More importantly, the EXTPROC process is able to dynamically load your developed DLL (or .so/.sl/.a file on UNIX).

So, architecturally it looks like this:

click to expand

Where:

  1. You have your initial connection to the database. You are running in either your dedicated server, or some shared server process.

  2. You make a call to an external procedure. Since this is your first call, your process will get in touch with the TNSLISTENER (Net8 listener) process.

  3. The Net8 listener will start (or find in a pool of already started) an external procedure process for you. This external procedure will load the requisite DLL (or .so/.sl/.a file on UNIX).

  4. You can now 'talk' to the external procedure process, which will marshal your data between SQL and C.

Configuring Your Server

We will now cover the requisite setup we must do to enable an external procedure to execute. This will involve setting up the LISTENER.ORA and TNSNAMES.ORA files on the server, not on the client machine. Upon install, these files should have automatically been configured for you with the external procedure (EXTPROC) services. If so, your LISTENER.ORA configuration file will resemble:

# LISTENER.ORA Network Configuration File: C:\oracle\network\admin\LISTENER.ORA # Generated by Oracle configuration tools.      LISTENER =   (DESCRIPTION_LIST =     (DESCRIPTION =       (ADDRESS_LIST =         (ADDRESS = (PROTOCOL = TCP)(HOST = tkyte-del)(PORT = 1521))       )       (ADDRESS_LIST =         (ADDRESS = (PROTOCOL = IPC)(KEY = EXTPROC1))       )     )   )      SID_LIST_LISTENER =   (SID_LIST =     (SID_DESC =       (SID_NAME = PLSExtProc)       (ORACLE_HOME = C:\oracle)       (PROGRAM = extproc)     )     (SID_DESC =       (GLOBAL_DBNAME = tkyte816)       (ORACLE_HOME = C:\oracle)       (SID_NAME = tkyte816)     )   ) 

These are the important things in the listener file for external procedures:

You may configure your LISTENER.ORA file manually using a plain text editor, or using the Net8 Assistant. It is strongly recommended that you use the Net8 assistant as the slightest configuration file error - such as a mis-matched parenthesis - will render these configuration files useless. If using the Net8 Assistant, follow the steps outlined in the online help under NetAssistantHelp, Local, Listeners, How To.., and finally Configure External Procedures for the Listener.

After modifying the LISTENER.ORA file remember to stop and start your listener using the commands Lsnrctl stop and Lsnrctl start on the command line.

The next file is the TNSNAMES.ORA file. This file needs to be in the directory that the server will use to resolve names. Typically, a TNSNAMES.ORA file is found on a client as it is used to find the server. This is one case where the server needs it to find a service itself. The TNSNAMES.ORA file will have an entry similar to:

# TNSNAMES.ORA Network Configuration File:C:\oracle\network\admin\TNSNAMES.ORA # Generated by Oracle configuration tools.      EXTPROC_CONNECTION_DATA =   (DESCRIPTION =     (ADDRESS_LIST =       (ADDRESS = (PROTOCOL = IPC)(KEY = EXTPROC1))     )     (CONNECT_DATA =       (SID = PLSExtProc)       (PRESENTATION = RO)     )   ) 

These are the important things in this configuration:

The following is a caveat on the EXTPROC_CONNECTION_DATA name. If your SQLNET.ORA specifies some default domain, it needs to be on the TNSNAMES entry. So if you have a SQLNET.ORA with a setting such as:

names.default_domain = world 

you would need to specify EXTPROC_CONNECTION_DATA.world, not just EXTPROC_CONNECTION_DATA in the TNSNAMES.ORA file.

Any errors in the above configuration files will almost certainly lead to the error ORA-28575 shown below:

declare * ERROR at line 1: ORA-28575: unable to open RPC connection to external procedure agent ORA-06512: at "USERNAME.PROCEDURE_NAME", line 0 ORA-06512: at line 5 

Upon receiving this error, the steps that might be useful to resolve this error are as follows:

We'll take an in depth look at each of these steps now. They assume you have actually hit this error for some reason.

Verify the extproc Program

The first sanity check after you've configured extprocs and received the ORA-28575 is to verify the existence and execution of the extproc program itself. This is easily done from the command line in either Windows or UNIX. You should do this when logged in with the credentials of the user that will be starting the listener (since that is the process that will execute the extproc), to verify that execute permissions for this user are in place. You will simply do the following:

C:\oracle\BIN>.\extproc.exe           Oracle Corporation --- SATURDAY  AUG 05 2000 14:57:19.851      Heterogeneous Agent based on the following module(s):    - External Procedure Module      C:\oracle\BIN> 

You are looking for output similar to the above. Note that I have run this from the [ORACLE_HOME]\bin directory as this is where we would find the extproc.exe program. If you cannot run this program, that would be an indication of a corrupted installation or some operating system configuration that must be corrected.

Verify the Database Environment

There are a couple of things to check in the database environment. First and foremost is to verify that the correct TNSNAMES.ORA is being used, and that it is configured correctly. For example, on my UNIX machine, using truss I can see that:

$ setenv TNS_ADMIN /tmp      $ truss sqlplus /@ora8i.us.oracle.com |& grep TNSNAMES      access("/export/home/tkyte/.TNSNAMES.ORA", 0)   Err#2 ENOENT access("/tmp/TNSNAMES.ORA", 0)                  Err#2 ENOENT access("/var/opt/oracle/TNSNAMES.ORA", 0)       Err#2 ENOENT access("/export/home/oracle8i/network/admin/TNSNAMES.ORA", 0) = 0 ... 

So, Oracle looked in:

for a TNSNAMES.ORA file. A common mistake is to configure the TNSNAMES.ORA file in the [ORACLE_HOME]/network/admin directory, but have a TNS_ADMIN environment variable that causes Oracle to find a different TNSNAMES.ORA elsewhere. So, make sure you have configured the correct TNSNAMES.ORA (if there is any confusion, you can simply set the TNS_ADMIN environment variable before starting the server; this will ensure the copy you want Oracle to use is in fact being used).

Once you have verified that the correct TNSNAMES.ORA is being used, we can look for configuration errors in that file. You would follow the steps outlined above to make sure the (ADDRESS = (PROTOCOL = IPC)(KEY = EXTPROC1)) and (CONNECT_DATA =(SID = PLSExtProc)) components are correctly set up. You do this by comparing them to the LISTENER.ORA configuration. If you use the Net8 Assistant to set this up, you will not have to concern yourself with matching parentheses. If you do it manually, be very careful. One mismatched, or out of place, parenthesis will make it impossible to use an entry.

Once you have verified these settings appear correct, we will look at the TNSNAMES.ORA entry name used. It must be EXTPROC_CONNECTION_DATA. It cannot be anything else, although it may have a domain appended to it. Verify the spelling of this entry. Check your SQLNET.ORA configuration file at this time. Oracle looks for the SQLNET.ORA file in the same way it looks for the TNSNAMES.ORA file. Beware that it does not have to be in the same directory as the TNSNAMES.ORA file - it may be in one of the other locations. If it either of:

are set, we must verify that we are in compliance with them.

If we have the names.default_domain set for example to (WORLD), we must ensure this domain appears in the TNSNAMES.ORA entry. Instead of EXTPROC_CONNECTION_DATA, it must be EXTPROC_CONNECTION_DATA.WORLD.

If we have the names.directory_path set we must verify that it contains TNSNAMES in it. If the names.directory_path is set to (HOSTNAME,ONAMES) for example, then Net8 will use the Host Naming Method to resolve the EXTPROC_CONNECTION_DATA connect string, and then the Oracle Names Server after that fails. Since neither of these methods will find EXTPROC_CONNECTION_DATA, our connection will fail, and the extproc call will fail. Simply add TNSNAMES to this list to allow Oracle to see the EXTPROC_CONNECTION_DATA entry in the local TNSNAMES.ORA file.

Verify the Listener

The issues for the listener are similar to the issues for the database environment. There are two things to consider when verifying the listener configuration:

Again, just as we did with the database server environment, we must ensure we are using the correct environment for the listener, allowing it to discover the LISTENER.ORA we want it to. We have the same considerations, in that the listener will look in various places for the configuration files. If there is any doubt as to which set of configuration files is being used, the TNS_ADMIN environment variable should be set prior to starting the listener. This will ensure that the configuration files we want it to use are in fact being used. Once you have verified that the correct configuration files are being used, you would use the information above with regards to the LISTENER.ORA file to ensure it is properly configured.

Once this is done, we should be able to, from the database server environment, use tnsping to tnsping extproc_connection_data (with any default domain appended). For example, my default domain is us.oracle.com and I am able to:

C:\oracle\network\ADMIN>tnsping extproc_connection_data.us.oracle.com      TNS Ping Utility for 32-bit Windows: Version 8.1.6.0.0 - Production on 06-AUG-2000 09:34:32      (c) Copyright 1997 Oracle Corporation.  All rights reserved.      Attempting to contact (ADDRESS=(PROTOCOL=IPC)(KEY=EXTPROC1)) OK (40 msec) 

This indicates and verifies that the database server and listener environment is correctly configured. We should have no problems with the ORA-28575: unable to open RPC connection to external procedure agent at this point.

The First Test

It is recommended to test the installation of external procedures with the demonstration programs. The reason for this is two-fold:

The demonstration program is found in your [ORACLE_HOME]/plsql/demo directory on all releases of Oracle 8i. The steps we should follow to make the demonstration are exposed in the following sections.

Compile extproc.c Code

First, we compile the extproc.c code into a DLL or .so/.sl/.a file. The process for doing this on Windows is to simply cd to the ORACLE_HOME\plsql\demo directory and type make (Oracle has supplied a make.bat file in that directory):

C:\oracle\plsql\demo>make Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 10.00.5270 for 80x86 Copyright (C) Microsoft Corp 1984-1995. All rights reserved.      extproc.c Microsoft (R) 32-Bit Incremental Linker Version 3.00.5270 Copyright (C) Microsoft Corp 1992-1995. All rights reserved.      /out:extproc.dll /dll /implib:extproc.lib /debug ..\..\oci\lib\msvc\oci.lib msvcrt.lib /nod:libcmt /DLL /EXPORT:UpdateSalary /EXPORT:PercentComm /EXPORT:PercentComm_ByRef /EXPORT:EmpExp /EXPORT:CheckEmpName /EXPORT:LobDemo extproc.obj C:\oracle\plsql\demo> 

In UNIX, you will do much the same thing, but the command to compile is different. There, it will look like:

$ make -f demo_plsql.mk extproc.so /usr/ccs/bin/make -f /export/home/ora816/rdbms/demo/demo_rdbms.mk extproc_callback SHARED_LIBNAME=extproc.so OBJS="extproc.o" ... 

After the command completes, you will have a .dll file on Windows or a .so/.sl/.a file on UNIX; the extension depends on platform. For example, Solaris is .so, HP/UX will be .sl.

Set Up the SCOTT/TIGER Account

For this demonstration program to work correct, we will need a SCOTT/TIGER demo account. If your database does not have a SCOTT/TIGER account, you can set one up by issuing:

SQL> grant connect, resource to scott identified by tiger; 

This will create the SCOTT user, and give it the ability to connect to the database and create objects like tables and packages. You will most like want to assign this to use a default tablespace other than SYSTEM, and a temporary tablespace as well.

SQL> alter user scott default tablespace tools temporary tablespace temp; 

Given that we have the SCOTT/TIGER account, we must provide it with one additional grant before proceeding. The SCOTT user will need the CREATE LIBRARY privilege. This privilege will allow SCOTT to issue the create library statement needed for external procedures. We will discuss this later. As this is a fairly powerful privilege, you will want to consider revoking it from SCOTT after running the example. To accomplish this you will need the following line:

SQL> grant create library to SCOTT; 

when connected as someone who has the CREATE LIBRARY privilege with the ADMIN option themselves (for example, SYSTEM or some other DBA account).

Lastly, you will want to ensure the EMP/DEPT demo tables are in the SCOTT schema and populated with data. You can verify this via:

SQL> select count(*) from emp;        COUNT(*) ----------         14      SQL> select count(*) from dept;        COUNT(*) ----------          4 

If these tables do not exist or do not have any data, you can rebuild them by executing demodrop.sql (to remove them) and demobld.sql (to create and populate them). These scripts are found in [ORACLE_HOME]\sqlplus\demo, and are meant to be executed via SQL*PLUS when logged in as SCOTT.

Create the demolib Library

The next step in the demonstration is to create the library object in Oracle. This object is simply a mapping of a library name (some 30 character name you choose) to a physical operating system file. This OS file is your compiled binary we made in the first step. The user who issues the CREATE LIBRARY statement must have the CREATE LIBRARY privilege granted to them, either via a ROLE or directly. This privilege is considered a fairly powerful one and should be given out only to those accounts you trust with the privilege. It will allow them to execute any arbitrary C code they wish on your server machine using the account the extproc service executes with. This is one of the reasons you would want to configure the extproc service to execute under some account other than the Oracle software owner (to avoid the inadvertent or malicious overwriting of your SYSTEM tablespace for example).

In order to accomplish this step, you use SQL*PLUS and execute:

SQL> connect scott/tiger Connected. SQL> create or replace library demolib as   2  'c:\oracle\plsql\demo\extproc.dll';   3  /      Library created. 

The name DEMOLIB is what the developers of the demo chose as the name of their library; you must use DEMOLIB. The file name c:\oracle\plsql\demo\extproc.dll may be different for you - I built the example directly in the demo directory of my ORACLE_HOME. You may have a different ORACLE_HOME than I do, or you might have built the demo in some other directory entirely. You should use the actual path name of the extproc.dll you built in the first step.

Installing and Running

The last step in the demo is to install the PL/SQL code that maps to routines in the demolib library. We are not interested at this point in what they look like as much as we are in what they output. We are using this demo to test external procedures. We'll look at how we code them soon.

Now we will simply execute:

SQL> connect scott/tiger Connected. SQL> @extproc 

when in the [ORACLE_HOME]\plsql\demo directory. What we expect to see is:

SQL> @extproc      Package created.      No errors.      Package body created.      No errors. ENAME      : ALLEN JOB        : SALESMAN SALARY     : 1600 COMMISSION : 300 Percent Commission : 18.75 ENAME      : MARTIN JOB        : SALESMAN SALARY     : 1250 COMMISSION : 1400 Percent Commission : 112 Return value from CheckEmpName : 0 old_ename value on return      : ANIL ENAME      : 7369 HIREDATE   : 17-DEC-80 Employee Experience Test Passed. ***************************************      PL/SQL procedure successfully completed.   (other feedback would be here as well)  

This shows that external procedures are correctly configured, and ready to be used on the system. The first procedure executes many of the routines in the extproc.dll we created. This shows conclusively that all is configured correctly.

In the event of an incorrectly configured system, you would expect to see:

SQL> @extproc      Package created.      No errors.      Package body created.      No errors. BEGIN demopack.demo_procedure; END;      * ERROR at line 1: ORA-28575: unable to open RPC connection to external procedure agent ORA-06512: at "SCOTT.DEMOPACK", line 61 ORA-06512: at "SCOTT.DEMOPACK", line 103 ORA-06512: at line 1 

This implies that it is time to revisit the section earlier on Configuring Your Server, and run through the verification steps.

Our First External Procedure

Given that our development environment is set up as above and ready to go for external procedures, we will now develop our first. This external procedure example will simply pass many different types of variables (strings, numbers, dates, arrays, and so on) and show how the corresponding C code would look to receive them. Our external procedure will manipulate some of these variables, changing the values of OUT or IN/OUT parameters, based on the values of some of the IN or IN/OUT parameters.

I will demonstrate how I prefer to map these variables, as there are many possible mappings and shortcuts. I am showing the method I myself prefer to use which, while more verbose than absolutely necessary, provides me with the maximum information at run-time. Additionally, I will introduce a template upon which I built all of my external procedures. This template implements many of the constructs you need to use in any real application such as:

The Wrapper

The way I like to start is with the PL/SQL prototype C I come up with the specification of the PL/SQL routines I would like to implement. The routines I would like to implement in this example will be a series of procedures that accept an IN parameter and an OUT (or IN/OUT) parameter. We will write one for each type of interesting data type (the ones we will use frequently). They will demonstrate how to pass as inputs and receive as outputs each of these types correctly. Additionally, I would like to have some functions that show how to return some of these interesting types as well. To me, the interesting types are:

In order to do this, we'll need to set up some collection types first. These represent our arrays of STRINGs, NUMBERs and DATEs:

tkyte@TKYTE816> create or replace type numArray as table of number   2  / Type created.      tkyte@TKYTE816> create or replace type dateArray as table of date   2  / Type created.      tkyte@TKYTE816> create or replace type strArray as table of varchar2(255)   2  / Type created. 

Now we are ready for the package specification. This is a series of overloaded procedures to test passing parameters with. Each routine has an IN and an OUT parameter with the exception of the CLOB version which has an IN/OUT parameter. The client must initialize the LOB IN OUT implementation and the external procedure will fill it in:

tkyte@TKYTE816> create or replace package demo_passing_pkg   2  as   3      procedure pass( p_in in number, p_out out number );   4   5      procedure pass( p_in in date, p_out out date );   6   7      procedure pass( p_in in varchar2, p_out out varchar2 );   8   9      procedure pass( p_in in boolean, p_out  out boolean );  10  11      procedure pass( p_in in CLOB, p_out in out CLOB );  12  13      procedure pass( p_in in numArray, p_out out numArray );  14  15      procedure pass( p_in in dateArray, p_out out dateArray );  16  17      procedure pass( p_in in strArray, p_out out strArray ); 

We cannot use overloading on the RAW and INT procedures below as we did above since PASS(RAW, RAW) would be confused with PASS(VARCHAR2,VARCHAR2) and PASS(INT,INT) gets confused with PASS(NUMBER,NUMBER) by Oracle. Therefore, we make an exception for these two and create named routines for them:

 19      procedure pass_raw( p_in in RAW, p_out out RAW );  20  21      procedure pass_int( p_in   in binary_integer,  22                          p_out  out binary_integer ); 

And lastly, we'll implement some functions to return values in order to demonstrate how that works. We'll implement one to return each type of interesting SCALAR type:

 25      function return_number return number;  26  27      function return_date return date;  28  29      function return_string return varchar2;  30  31  end demo_passing_pkg;  32  /      Package created. 

The CREATE TYPES represent our array types. We have defined new SQL types; numArray to be a nested table type of numbers, dateArray as a nested table of dates, and strArray as a nested table of VARCHAR2(255)s. We have now created the spec of the package we want to implement so we're ready for the rest of it. I'll introduce this piece-by-piece now. We'll start with the library definition:

tkyte@TKYTE816> create or replace library demoPassing   2  as   3  'C:\demo_passing\extproc.dll'   4  / Library created. 

That, as you might recall from testing the SCOTT/TIGER example above, simply defines to Oracle where the library named demoPassing physically is stored C in this case, the file C:\demo_passing\extproc.dll. The fact that we have not yet built this DLL is not relevant yet. We need the library object in order to compile the PL/SQL body that we are going to create. The extproc.dll will come later. We can execute the create library successfully without actually having the library yet.

Now, onto the package body:

tkyte@TKYTE816> create or replace package body demo_passing_pkg   2  as   3   4      procedure pass(  p_in    in  number,   5                       p_out   out number )   6      as   7      language C   8      name "pass_number"   9      library demoPassing  10      with context  11      parameters (  12                   CONTEXT,  13                   p_in   OCINumber,  14                   p_in   INDICATOR short,  15                   p_out  OCINumber,  16                   p_out  INDICATOR short ); 

So, in looking at that, it starts off normal enough with CREATE OR REPLACE PACKAGE and PROCEDURE Pass( ... ) as ..., but then it diverges from a normal stored procedure. We are creating a call specification now, not embedding PL/SQL code. A call specification is the method by which Oracle maps the PL/SQL types to the external routine's native data types. For example, the above is mapping the p_in number parameter to the C data type OCINumber. A line-by-line explanation of what we are doing in this case is:

Given that, we will skip ahead for a moment and look at the C prototype that will go with this SQL call specification we've just created. Given the above input, our C prototype will be:

 18       --void pass_number  19       --  (  20       -- OCIExtProcContext  * ,  /*   1 : With-Context  */  21       --         OCINumber  * ,  /*   2 : P_IN  */  22       --             short    ,  /*   3 : P_IN (Indicator) */  23       --         OCINumber  * ,  /*   4 : P_OUT  */  24       --             short  *    /*   5 : P_OUT (Indicator) */  25       --  ); 

Now, we will finish looking at the annotated PL/SQL package body that maps the rest of the procedures/functions. Here is the routine that passes Oracle dates back and forth; they will be mapped to the C datatype OCIDate supplied by OCI:

 27      procedure pass( p_in in date, p_out out date )  28      as  29      language C name "pass_date" library demoPassing  30      with context parameters  31      ( CONTEXT,  32        p_in   OCIDate, p_in   INDICATOR short,  33        p_out  OCIDate, p_out  INDICATOR short );  34  35       -- void pass_date  36       --   (  37       --  OCIExtProcContext  *,  /*   1 : With-Context  */  38       --            OCIDate  *,  /*   2 : P_IN  */  39       --              short   ,  /*   3 : P_IN (Indicator) */  40       --            OCIDate  *,  /*   4 : P_OUT  */  41       --              short  *   /*   5 : P_OUT (Indicator) */  42       --   ); 

Next, we see how to pass the varchar2 type back and forth C in this case Oracle will map the STRING type of a C char * C a character string pointer.

 45      procedure pass(  p_in in varchar2, p_out  out  varchar2 )  46      as  47      language C name "pass_str" library demoPassing  48      with context parameters  49      ( CONTEXT,  50        p_in   STRING, p_in  INDICATOR short,  51        p_out  STRING, p_out INDICATOR short, p_out  MAXLEN int );  52  53       -- void pass_str  54       --   (  55       --  OCIExtProcContext  *,  /*   1 : With-Context  */  56       --               char  *,  /*   2 : P_IN  */  57       --              short   ,  /*   3 : P_IN (Indicator) */  58       --               char  *,  /*   4 : P_OUT  */  59       --              short  *,  /*   5 : P_OUT (Indicator) */  60       --                int  *   /*   6 : P_OUT (Maxlen) */  61       --   ); 

In the above, we see the first use of MAXLEN. This instructs Oracle to pass to our external procedure the maximum width of the OUT parameter p_out. Since we are returning a string, it is very useful to know what the maximum length of that string may be, to avoid a buffer overwrite. For all string types that are mapped as an OUT parameter the use of the MAXLEN parameter is strongly encouraged.

Next, we see how to pass the PL/SQL BOOLEAN type; this will be mapped to a C int type:

 64      procedure pass( p_in in boolean, p_out out boolean )  65      as  66      language C name "pass_bool" library demoPassing  67      with context parameters  68      ( CONTEXT,  69        p_in  int, p_in  INDICATOR short,  70        p_out int, p_out INDICATOR short );  71  72       -- void pass_bool  73       --   (  74       --  OCIExtProcContext  *,  /*   1 : With-Context  */  75       --                int   ,  /*   2 : P_IN  */  76       --              short   ,  /*   3 : P_IN (Indicator) */  77       --                int  *,  /*   4 : P_OUT  */  78       --              short  *   /*   5 : P_OUT (Indicator) */  79       --   ); 

Next, we see the CLOB example. Here we pass the PL/SQL CLOB type to a C OCILobLocator type. Notice in this case, we must be ready to receive a pointer to a pointer for the OUT parameter. This allows the C code to change not only the contents of what the LOB locator points to, but also change the LOB locator itself C allowing us to point it to a totally different LOB if we wanted to:

 83      procedure pass( p_in in clob, p_out in out clob )  84      as  85      language C name "pass_clob" library demoPassing  86      with context parameters  87      ( CONTEXT,  88        p_in   OCILobLocator, p_in   INDICATOR short,  89        p_out  OCILobLocator, p_out  INDICATOR short );  90  91       -- void pass_clob  92       --   (  93       --  OCIExtProcContext  *,  /*   1 : With-Context  */  94       --      OCILobLocator  *,  /*   2 : P_IN  */  95       --              short   ,  /*   3 : P_IN (Indicator) */  96       --      OCILobLocator **,  /*   4 : P_OUT  */  97       --              short  *   /*   5 : P_OUT (Indicator) */  98       --   ); 

Next are the three routines that pass arrays, Oracle collection types, back and forth. Since the mappings for each are so similar C we'll present all three at once. The C routines for each have the same exact prototypes C the OCIColl type is passed to each, regardless of the collection type being passed:

100      procedure pass( p_in in numArray, p_out out numArray ) 101      as 102      language C name "pass_numArray" library demoPassing 103      with context parameters 104      ( CONTEXT, 105        p_in  OCIColl, p_in  INDICATOR short, 106        p_out OCIColl, p_out INDICATOR short ); 107 108       -- void pass_numArray 109       --   ( 110       --  OCIExtProcContext  *,  /*   1 : With-Context  */ 111       --            OCIColl  *,  /*   2 : P_IN  */ 112       --              short   ,  /*   3 : P_IN (Indicator) */ 113       --            OCIColl **,  /*   4 : P_OUT  */ 114       --              short  *   /*   5 : P_OUT (Indicator) */ 115       --   ); 116 117      procedure pass( p_in in dateArray, p_out out dateArray ) 118      as 119      language C name "pass_dateArray" library demoPassing 120      with context parameters 121      ( CONTEXT, 122        p_in  OCIColl, p_in  INDICATOR short, 123        p_out OCIColl, p_out INDICATOR short ); 124 125      procedure pass( p_in in strArray, p_out out strArray ) 126      as 127      language C name "pass_strArray" library demoPassing 128      with context parameters 129      ( CONTEXT, 130        p_in  OCIColl, p_in  INDICATOR short, 131        p_out OCIColl, p_out INDICATOR short ); 

Next we have the routine that passes raw data back and forth. Here, we use both the MAXLEN (which we observed with the VARCHAR2s above) and the LENGTH attribute. We must pass a length with a RAW type as RAWs contain binary information, including binary zeroes, which means our C program would not be able to determine the actual string length, and Oracle would not be able to figure out how big the raw data we were returning is. For RAW types both LENGTH and MAXLEN are crucial. LENGTH must be passed and MAXLEN should be passed.

134      procedure pass_raw(  p_in in raw, p_out out raw ) 135      as 136      language C name "pass_raw " library demoPassing 137      with context parameters 138      ( CONTEXT, 139        p_in  RAW, p_in  INDICATOR short, p_in  LENGTH  int, 140        p_out RAW, p_out INDICATOR short, p_out MAXLEN int, 141        p_out LENGTH int ); 142       -- void pass_long_raw 143       --   ( 144       --  OCIExtProcContext  *,  /*   1 : With-Context  */ 145       --      unsigned char  *,  /*   2 : P_IN  */ 146       --              short   ,  /*   3 : P_IN (Indicator) */ 147       --                int   ,  /*   4 : P_IN (Length) */ 148       --      unsigned char  *,  /*   5 : P_OUT  */ 149       --              short  *,  /*   6 : P_OUT (Indicator) */ 150       --                int  *,  /*   7 : P_OUT (Maxlen) */ 151       --                int  *   /*   8 : P_OUT (Length) */ 152       --   ); 

Next we have the routine to pass a PL/SQL BINARY_INTEGER back and forth to C. In this case, the BINARY_INTEGER type is mapped to the native int type in C:

154    procedure pass_int(p_in in binary_integer, p_out out binary_integer ) 155    as 156    language C name "pass_int" library demoPassing 157    with context parameters 158    ( CONTEXT, 159      p_in  int, p_in  INDICATOR short, 160      p_out int, p_out INDICATOR short ); 161 162       -- void pass_int 163       --   ( 164       --  OCIExtProcContext  *,  /*   1 : With-Context  */ 165       --                int   ,  /*   2 : P_IN  */ 166       --              short   ,  /*   3 : P_IN (Indicator) */ 167       --                int  *,  /*   4 : P_OUT  */ 168       --              short  *   /*   5 : P_OUT (Indicator) */ 169       --   ); 

Here are the wrappers for our three functions to return NUMBERs, DATEs, and STRINGs. We encounter a new keyword here; RETURN. When we map to a function, we must use the RETURN keyword as the last parameter in the parameter list. It is actually the return type of the function and not a formal parameter to/from the function itself. In the above, I have three parameters in the SQL wrapper, but only two parameters in the C prototype I will be using. The RETURN OCINumber parameter really defines the type I will be returning from the function, as denoted by OCINumber *return_number. I do include an indicator even for my return value, since I want to be able to return Null at some time. If I did not include this indicator, I would have no mechanism to be able to return a Null value. As we will see with the string example, I can return the LENGTH attribute as well, but not MAXLEN, as this is only available with OUT parameters where the Oracle sets up storage. With return values, since we are responsible for allocating memory, the MAXLEN attribute would not make sense.

173      function return_number return number 174      as 175      language C name "return_number" library demoPassing 176      with context parameters 177      ( CONTEXT, RETURN INDICATOR short, RETURN OCINumber ); 178 179       -- OCINumber *return_number 180       --   ( 181       --  OCIExtProcContext  *,  /*   1 : With-Context  */ 182       --              short  *   /*   2 : RETURN (Indicator) */ 183       --   ); 184 185    function return_date return date 186    as 187    language C name "return_date" library demoPassing 188    with context parameters 189    ( CONTEXT, RETURN INDICATOR short, RETURN OCIDate ); 190 191       -- OCIDate *return_date 192       --   ( 193       --  OCIExtProcContext  *,  /*   1 : With-Context  */ 194       --              short  *   /*   2 : RETURN (Indicator) */ 195       --   ); 196 197    function return_string return varchar2 198    as 199    language C name "return_string" library demoPassing 200    with context parameters 201    (CONTEXT, RETURN INDICATOR short, RETURN LENGTH int, RETURN STRING ); 202 203        -- char *return_string 204        --   ( 205        --  OCIExtProcContext  *,  /*   1 : With-Context  */ 206        --              short  *,  /*   2 : RETURN (Indicator) */ 207        --                int  *   /*   3 : RETURN (Length) */ 208        --   ); 209 210  end demo_passing_pkg; 211  /      Package body created. 

There you can see in last function, that I am using the LENGTH attribute as well as the INDICATOR. This is so I can let Oracle know how long the string I'm returning is.

So, to recap, what we have now is:

At this point after seeing the example, it might be a good time to take a look at a data type mapping table. That is, when given a SQL type X, what external types are available? Then, given the external type we choose, what is the C type that will actually be used. These data type mapping tables are available in the Oracle Application Developers Guide C Fundamentals and are included here for your reference. Note that an external data type is not a C type (nor is it a SQL or PL/SQL type). You will need to refer to the second table to get the real C type you should use.

SQL or PL/SQL Data Type

External Data Type

Default Type

BINARY_INTEGER, BOOLEAN, PLS_INTEGER

[unsigned]char, [unsigned]short, [unsigned]int, [unsigned]long, sb1, sb2, sb4, ub1, ub2, ub4, size_t

int

NATURAL, NATURALN, POSITIVE, POSITIVEN, SIGNTYPE

[unsigned]char, [unsigned]short, [unsigned]int, [unsigned]long, sb1, sb2, sb4, ub1, ub2, ub4, size_t

unsigned int

FLOAT, REAL

Float

float

DOUBLE PRECISION

Double

double

CHAR, CHARACTER, LONG, NCHAR, NVARCHAR2, ROWID, VARCHAR2, VARCHAR

string, ocistring

string

LONG RAW, RAW

raw, ociraw

raw

BFILE, BLOB, CLOB, NCLOB

ociloblocator

ociloblocator

NUMBER, DEC, DECIMAL, INT, INTEGER, NUMERIC, SMALLINT

[unsigned]char, [unsigned]short, [unsigned]int, [unsigned]long, sb1, sb2, sb4, ub1, ub2, ub4, size_t, ocinumber

ocinumber

DATE

Ocidate

ocidate

Abstract Data Types (ADTs)

Dvoid

dvoid

Collections (nested tables, VARRAYS)

Ocicoll

ocicoll

So, the above table maps the SQL or PL/SQL types to an external data type. I recommend always using the default type as listed above as they are the easiest to handle in your C code. An external data type looks a lot like a C type but we need to go one step further. Since each SQL or PL/SQL type may be in, in out, out, or a return value from a function, we must go one step further in determining the actual C Type. In general, variables that are 'returned' or are in are passed by value, whereas variables that are in out, out or passed explicitly by reference are passed via pointers, by reference. The following table shows us what C type we would use given an external data type and a parameter mode:

External Data Type

C Type for IN Parameters and RETURN values

C Type for IN OUT, OUT and by REFERENCE Variables

[unsigned] char

[unsigned] char

[unsigned] char *

[unsigned] short

[unsigned] short

[unsigned] short *

[unsigned] int

[unsigned] int

[unsigned] int *

[unsigned] long

[unsigned] long

[unsigned] long *

size_t

size_t

size_t *

sb1

sb1

sb1 *

sb2

sb2

sb2 *

sb4

sb4

sb4 *

ub1

ub1

ub1 *

ub2

ub2

ub2 *

ub4

ub4

ub4 *

float

float

float *

double

double

double *

string

char *

char *

raw

unsigned char *

unsigned char *

Ociloblocator

OCILobLocator *

OCILobLocator * *

Ocinumber

OCINumber *

OCINumber *

Ocistring

OCIString *

OCIString *

Ociraw

OCIRaw *

OCIRaw *

Ocidate

OCIDate *

OCIDate *

Ocicoll

OCIColl *

OCIColl **

Adt

dvoid *

dvoid *

The C Code

Now we are ready for the C code to implement our library. We will start with the common template I use for all external procedures. This template includes standard header files I frequently use, the Oracle OCI header file we need, and three functions debugf, oci_error, and raise_application_error. These functions will help us in meeting our goals of having a tracing mechanism (debugf) and generic error handling (oci_error and raise_application_error). I simply copy this file any time I'm starting a new extproc project, and use it to start.

#include <stdio.h> #include <stdlib.h> #include <stdarg.h> #include <time.h> #include <string.h> #include <errno.h> #include <ctype.h>      #include <oci.h>      #ifdef WIN_NT #define INI_FILE_NAME "c:\\temp\\extproc.ini" #else #define INI_FILE_NAME "/tmp/extproc.ini" #endif      #define strupr(a) {char * cp; for(cp=a;*cp;*cp=toupper(*cp), cp++);} 

The above is the very beginning of my C template. I include 'popular' header files, ones that I use frequently, as well as define where my parameter file will be. There are many ways we could set this at run-time. For example, if I was building an external procedure for execution on Windows, I might use the Windows functions RegOpenKeyEx, RegQueryInfoKey, and RegEnumValue to retrieve the location of the parameter file from the registry. On UNIX, I might use an environment variable. In this example, I simply 'hard code' the location into the external procedure. This is a valid approach as you could simply demand that your initialization parameter file be placed in some known location (for example, /etc/your_extproc.ora on UNIX, and c:\your_extproc\your_extproc.ora on Windows).

Now onto the code itself. The next part defines our context. It holds what normally might be global variables in a typical program. We cannot (should not C it is wholly unreliable) use global variables in an external procedure. Also, since static data will be reinitialized between calls globals would not work correctly anyway. We will use the OCI context management API calls to get and set a global context for our extproc. You would add any state variables you needed to preserve from call to call in this structure below yourself.

The global variables I have defined are:

typedef struct myCtx {   OCIExtProcContext * ctx;      /* Context passed to all external procs */   OCIEnv *            envhp;    /* OCI environment handle */   OCISvcCtx *         svchp;    /* OCI Service handle */   OCIError *          errhp;    /* OCI Error handle */        int                 curr_lineno;   char *              curr_filename;        ub1                 debugf_flag;   char                debugf_path[255];   char                debugf_filename[50];        /* add your own state variables here... */ }     myCtxStruct; 

Next in the source code template, we have debugf, our tracing routine. It is a C function that works very much like the standard C fprintf does and even accepts a varying number of input arguments (the in the argument list). The first argument to it is the 'context'; our state described above. I always assume this state pointer is named myCtx (my macro for debugf makes that assumption). This debugf routine shows a couple of things. It introduces much of the OCI file-handling API, which is modeled loosely after the C fopen/fread/fwrite/fclose API family. The debugf routine, which is only called if the flag myCtx->debugf_flag is actually set, simply opens a file, builds a message, writes it, and closes the file.

This shows how our context is used as well. It contains our 'session' state and holds important variables such as the OCIEnv and OCIError structures we need for all OCI API calls. It shows how we can set the state simply by manipulating variables in the structure (as the debugf macro does). The debugf macro will 'short circuit' calls to the actual _debugf() subroutine. By this, I mean that if either myCtx or myCtx->debugf_flag is not set, the context state is never changed, and the _debugf() routine is never called. This means that you can safely leave all of your debugging statements in your production code, as its existence does not materially affect run-time performance in the long run (when debugf_flag is set to false).

void _debugf( myCtxStruct * myCtx, char * fmt, ... ) { va_list         ap; OCIFileObject * fp; time_t          theTime = time(NULL); char            msg[8192]; ub4             bytes;          if ( OCIFileOpen( myCtx->envhp, myCtx->errhp, &fp,                       myCtx->debugf_filename,                       myCtx->debugf_path,                       OCI_FILE_WRITE_ONLY, OCI_FILE_APPEND|OCI_FILE_CREATE,                       OCI_FILE_TEXT ) != OCI_SUCCESS ) return;          strftime( msg, sizeof(msg),              "%y%m%d %H%M%S GMT ", gmtime(&theTime) );     OCIFileWrite( myCtx->envhp, myCtx->errhp, fp, msg, strlen(msg), &bytes );          va_start(ap,fmt);     vsprintf( msg, fmt, ap );     va_end(ap);     strcat( msg,"\n");          OCIFileWrite( myCtx->envhp, myCtx->errhp, fp, msg, strlen(msg), &bytes );     OCIFileClose( myCtx->envhp, myCtx->errhp, fp ); } 

This next bit of code introduces a macro interface to debugf. This macro is a more convenient way to use debugf. Instead of having to pass the _LINE_, _FILE_ each time we call; we just code:

    debugf( myCtx, "This is some format %s", some_string );

and this macro will set them in our context and then call _debugf for us.

void _debugf( myCtxStruct * myCtx, char * fmt, ... ); #define debugf  \ if ((myCtx!=NULL) && (myCtx->debugf_flag)) \     myCtx->curr_lineno = __LINE__, \     myCtx->curr_filename = __FILE__, \     _debugf 

Next in the template is an error handling utility, raise_application_error. This should be a familiar name to a PL/SQL developer. raise_application_error is a PL/SQL built-in function to raise errors as exceptions. This function has the same exact purpose. If your external procedure calls this function prior to returning, the return values from your external procedure are ignored, and an exception will be raised to the caller instead. This makes handling errors from an external procedure no different than any other PL/SQL routine as far as the caller is concerned.

static int raise_application_error(  myCtxStruct * myCtx,                                      int           errCode,                                      char *        errMsg, ...) { char    msg[8192]; va_list ap;          va_start(ap,errMsg);     vsprintf( msg, errMsg, ap );     va_end(ap);          debugf( myCtx,  "raise application error( %d, %s )", errCode, msg );     if ( OCIExtProcRaiseExcpWithMsg(myCtx->ctx,errCode,msg,0) ==                                                 OCIEXTPROC_ERROR )     {       debugf( myCtx,  "Unable to raise exception" );     }     return -1; } 

Next is another error handling routine, lastOciError. This function takes the current session context and, using the OCIError structure in it, retrieves the last OCI error text that occurred. It retrieves this text into some memory that has been allocated using OCIExtProcAllocCallMemory(). Any memory allocated by this function will be automatically freed for us upon returning from the external procedure. This function is most frequently used in a call to raise_application_error after a failed OCI call. It simply lets the caller know the cause of the OCI error we encountered.

static char * lastOciError( myCtxStruct * myCtx ) { sb4       errcode; char      * errbuf = (char*)OCIExtProcAllocCallMemory( myCtx->ctx, 256 );          strcpy( errbuf, "unable to retrieve message\n" );     OCIErrorGet( myCtx->errhp, 1, NULL, &errcode, errbuf,                  255, OCI_HTYPE_ERROR );     errbuf[strlen(errbuf)-1] = 0;     return errbuf; } 

Now for the 'workhorse' routine of the external procedure template; init. It is responsible for establishing and retrieving our state, and processing any parameters we have set up in our initialization file. It is a lot to digest in one fell swoop, but it is actually quite straightforward once we introduce the OCI API calls we are utilizing.

The goal of the init routine is to set up our myCtxStruct, and to call any OCI initialization functions we need. This function starts by getting the OCI environment handles. It does this in one of two ways. If we are using just OCI (no Pro*C) we simply call OCIExtProcGetEnv with the context that was sent to our external procedure. This OCI API retrieves these handles for us. If we are using both OCI and Pro*C, we must instead use EXEC SQL REGISTER CONNECT :ctx. This sets up the Pro*C layer. We still have to retrieve the OCI environment handles, but this time, we must use the Pro*C-supplied API calls to do that C SQLEnvGet, SQLSvcCtxGet. You would comment or uncomment the appropriate method depending on your needs.

/*------------- include this for Pro*C external procedures only!! -------- #define SQLCA_INIT EXEC SQL INCLUDE sqlca;   ------------------------------------------------------------------------*/      static myCtxStruct * init( OCIExtProcContext * ctx ) { ub1          false = 0; myCtxStruct *myCtx = NULL; OCIEnv      *envhp; OCISvcCtx   *svchp; OCIError    *errhp; ub4          key = 1;               if ( OCIExtProcGetEnv( ctx, &envhp, &svchp, &errhp ) != OCI_SUCCESS )     {          OCIExtProcRaiseExcpWithMsg(ctx,20000,                                    "failed to get OCI Connection",0);          return NULL;     }      /*----- replace the above OCIExtProcGetEnv() call with the following -----   ----- when using Pro*C -------------------------------------------------          EXEC SQL REGISTER CONNECT USING :ctx;     if ( sqlca.sqlcode < 0 )     {         OCIExtProcRaiseExcpWithMsg(ctx,20000,sqlca.sqlerrm.sqlerrmc,70);         return NULL;     }     if ( ( SQLEnvGet(0, &envhp ) != OCI_SUCCESS )  ||          ( OCIHandleAlloc(envhp, (dvoid**)&errhp,                         OCI_HTYPE_ERROR,0,0) != OCI_SUCCESS ) ||          ( SQLSvcCtxGet(0, NULL, 0, &svchp ) != OCI_SUCCESS ) )     {          OCIExtProcRaiseExcpWithMsg(ctx,20000,"failed to get OCI ENV",0);          return NULL;     }   ---------------------------------------------------------------------*/ 

Once we have the OCI environment, the first thing we do is call OCIContextGetValue() to retrieve our context. This API call takes the OCI environment and a 'key', and attempts to retrieve a pointer. The 'key' in this case is some 64-bit number. You may store as many contexts as you like, but we'll just be using one at this time.

    if ( OCIContextGetValue( envhp, errhp, (ub1*)&key, sizeof(key),                              (dvoid**)&myCtx ) != OCI_SUCCESS )     {         OCIExtProcRaiseExcpWithMsg(ctx,20000,"failed to get OCI Context",0);         return NULL;     } 

If we retrieve a Null pointer at this time, which simply indicates we have not set a context as yet, we will allocate sufficient memory for one and set it. The call to OCIMemoryAllocate is used to allocate a block of memory that will stay valid for the life of the process. Once we've allocated this memory, we save it in our context using OCIContextSetValue. This function will associate our pointer (which will never change) with the key we choose for the duration of our session. The very next call to OCIContextGetValue with the same key in the same session will retrieve this pointer for us.

    if ( myCtx == NULL )     {         if ( OCIMemoryAlloc( envhp, errhp, (dvoid**)&myCtx,                              OCI_DURATION_PROCESS,                              sizeof(myCtxStruct),                              OCI_MEMORY_CLEARED ) != OCI_SUCCESS )         {             OCIExtProcRaiseExcpWithMsg(ctx,20000,                                       "failed to get OCI Memory",0);             return NULL;         }         myCtx->ctx   = ctx;         myCtx->envhp = envhp;         myCtx->svchp = svchp;         myCtx->errhp = errhp;         if ( OCIContextSetValue( envhp, errhp,                                  OCI_DURATION_SESSION, (ub1*)&key,                                  sizeof(key), myCtx ) != OCI_SUCCESS )         {             raise_application_error(myCtx, 20000, "%s",                                     lastOciError(myCtx));             return NULL;         } 

Continuing onwards, since we retrieved a Null pointer that indicates we had never processed our parameters. We do so in the next block of code. We use the supplied parameter management API calls to process files that follow the same rules as the Oracle init.ora parameter file. See Chapter 2, Architecture for a description of those files. I typically use this file to control debug tracing, and any other state variable defaults I would like in my program. The initialization file we are using for example, might look like this:

debugf = true debugf_filename = extproc2.log debugf_path = /tmp/ 

This enables tracing (debugf = true) to a file /tmp/extproc2.log. You could add additional parameters to this file and modify the init code appropriate to read and set them in your session's context. The process for reading and processing a parameter file takes the following steps:

  1. Call OCIExtractInit to initialize the parameter processing library.

  2. Call OCIExtractSetNumKeys to let the OCI API know the number of key names you are going to ask for. This must match the number of parameters entered into the parameter file.

  3. Call OCIExtractSetKey the number of times you told OCIExtractSetNumKeys().

  4. Call OCIExtractFromFile to process the parameter file.

  5. Call OCIExtractTo<some datatype> to retrieve each of the parameter values in turn.

  6. Call OCIExtractTerm to terminate the parameter processing library and allow it to return any resources it might have allocated to the system.

        if (( OCIExtractInit( envhp, errhp ) != OCI_SUCCESS )  ||             ( OCIExtractSetNumKeys( envhp, errhp, 3 ) != OCI_SUCCESS ) ||             ( OCIExtractSetKey( envhp, errhp, "debugf",                                 OCI_EXTRACT_TYPE_BOOLEAN,                                 0, &false, NULL, NULL ) != OCI_SUCCESS ) ||             ( OCIExtractSetKey( envhp, errhp, "debugf_filename",                                 OCI_EXTRACT_TYPE_STRING,                                 0, "extproc.log",                                 NULL, NULL ) != OCI_SUCCESS )  ||             ( OCIExtractSetKey( envhp, errhp, "debugf_path",                                 OCI_EXTRACT_TYPE_STRING,                                 0, "", NULL, NULL ) != OCI_SUCCESS )  ||             ( OCIExtractFromFile( envhp, errhp, 0,                                   INI_FILE_NAME ) != OCI_SUCCESS ) ||             ( OCIExtractToBool( envhp, errhp, "debugf", 0,                                 &myCtx->debugf_flag ) != OCI_SUCCESS ) ||             ( OCIExtractToStr( envhp, errhp, "debugf_filename", 0,                                myCtx->debugf_filename,                                sizeof(myCtx->debugf_filename ) )                                                      != OCI_SUCCESS ) ||             ( OCIExtractToStr( envhp, errhp, "debugf_path",                                0, myCtx->debugf_path,                                sizeof(myCtx->debugf_path ) )                                                      != OCI_SUCCESS ) ||             ( OCIExtractTerm( envhp, errhp ) != OCI_SUCCESS ))         {             raise_application_error(myCtx, 20000, "%s",                                        lastOciError(myCtx));             return NULL;         }     } 

The following is the block of code that will be executed on the second and subsequent calls to init by our session. Since OCIContextGetValue returns our context for the second and subsequent calls, we simply set up our data structure to point to it:

    else     {         myCtx->ctx   = ctx;         myCtx->envhp = envhp;         myCtx->svchp = svchp;         myCtx->errhp = errhp;     } 

The last thing we do in init before returning, is to call the OCIFileInit routine. This initializes the OCI file-handling API for us, and makes it so we can open and read/write OS files. We could use the standard C fopen, fclose, fread, fwrite routines. This just makes it a little more portable and makes handling errors very consistent from platform-to-platform. You may add additional init calls here yourself as well. For example, if you choose to use the OCIFormat* (similar to vsprintf in C) routines, you could add a call to OCIFormatInit here. Don't forget to add a corresponding OCIFormatTerm call to your term routine below as well.

    if ( OCIFileInit( myCtx->envhp, myCtx->errhp ) != OCI_SUCCESS )     {         raise_application_error(myCtx, 20000, "%s", lastOciError(myCtx));         return NULL;     }     return myCtx; } 

Now for the term referred to above. This is my termination routine, a cleanup routine, and it must be called after any successful call to init above. It should be the last thing you call in your routine before returning from C to SQL:

static void term( myCtxStruct * myCtx ) {     OCIFileTerm( myCtx->envhp, myCtx->errhp ); } 

That's the end of my template. I use the same source code template for every external procedure project I do (minor modifications for pure OCI versus Pro*C plus OCI). This saves a lot of time and provides a lot of functionality.

Now we start adding our code. The first part I start with, right after the generic component, is a listing of all error codes we will return, beginning with 20001. It is convenient to list them all here, as it will allow us to easily set up a pragma exception_init mapping in PL/SQL for each error code in our PL/SQL. This will let PL/SQL programs catch named exceptions instead of having to inspect error codes. We will not demonstrate this with this particular example; however, when we get to the Pro*C example below we will. Error numbers must be in the range of 20,000 to 20,999 as these are the ones supported by Oracle; the remaining error codes are reserved for its use.

#define ERROR_OCI_ERROR     20001 #define ERROR_STR_TOO_SMALL 20002 #define ERROR_RAW_TOO_SMALL 20003 #define ERROR_CLOB_NULL     20004 #define ERROR_ARRAY_NULL    20005 

Next comes our first real routine. This procedure is the implementation of the pass_number routine we specced out in PL/SQL above. It takes a PL/SQL Number IN and sets an OUT PL/SQL NUMBER. Our sample routine below will:

We will also see a portability macro we place in front of each routine that is callable from PL/SQL. This macro will 'export' a function. This is needed only on the Windows platform, and is technically not necessary on UNIX. I typically put it in regardless of the platform I am building on, as the need to move the extproc library from Windows to UNIX, and vice versa arises frequently for me. Doing this all of the time just makes it easier. The embedded comments explain the code in detail as we go along:

#ifdef WIN_NT _declspec (dllexport) #endif void pass_number  ( OCIExtProcContext * ctx       /* CONTEXT */,         OCINumber *        p_inum     /* OCINumber */,    short              p_inum_i   /* INDICATOR short */,         OCINumber *        p_onum     /* OCINumber */,    short *            p_onum_i   /* INDICATOR short */ ) { double     l_inum; myCtxStruct*myCtx; 

Before we can do anything, we must retrieve our session context. This gets the OCI environment, our parameters, and so on. It will be the first call we make in all of our extproc routines:

    if ( (myCtx = init( ctx )) == NULL ) return;     debugf( myCtx,  "Enter Pass Number" ); 

Now, we'll access the first parameter. We passed it as an OCINumber type. We now can use the many OCINumber* functions on it. In this case, we'll get the Oracle Number converted into a C DOUBLE using OCINumberToReal. We could convert into an Int, Long, Float, or formatted string just as easily.

First, we must check to see if the passed Number is Not Null, if so process, else in this case; do nothing except call term() and return. If we successfully access the first parameter, we negate it and then construct an OCINumber from it using OCINumberFromReal. If that is successful, we set the null indicator p_onum_I to not Null C to let the caller know there is a value stored in there. Then we are done, we call term to clean up and return:

    if ( p_inum_i == OCI_IND_NOTNULL )     {        if ( OCINumberToReal( myCtx->errhp, p_inum, sizeof(l_inum), &l_inum )                  != OCI_SUCCESS )        {             raise_application_error(myCtx,ERROR_OCI_ERROR,                                     "%s",lastOciError(myCtx));        }        else        {            debugf( myCtx,  "The first parameter is %g", l_inum );            l_inum = -l_inum;            if ( OCINumberFromReal( myCtx->errhp, &l_inum,                                     sizeof(l_inum), p_onum) != OCI_SUCCESS )            {                 raise_application_error(myCtx,ERROR_OCI_ERROR,                                         "%s",lastOciError(myCtx));            }            else            {                *p_onum_i = OCI_IND_NOTNULL;                debugf( myCtx,                     "Set OUT parameter to %g and set indicator to NOTNULL",                         l_inum );            }         }     }     term(myCtx); } 

and that's it. Our first routine makes use of all of our helper routines; raise_application_error, lastOciError, init, term, and debugf. Later, when we test this routine, we will inspect the results of our debugf calls. They will confirm that routine does what it is supposed to (and provide a handy tool for debugging later on).

Note how I am careful to return from only one location in this routine. If you return from more than one place, make sure to call term(myCtx) at each location.

Now, we move onto the remaining routines. The next routine deals with dates as IN and OUT parameters. We will:

#ifdef WIN_NT _declspec (dllexport) #endif void pass_date  ( OCIExtProcContext * ctx        /* CONTEXT */,         OCIDate *          p_idate    /* OCIDATE */,    short              p_idate_i  /* INDICATOR short */,         OCIDate *          p_odate    /* OCIDATE */,    short *            p_odate_i  /* INDICATOR short */  ) { char      buffer[255]; ub4       buff_len; char      * fmt = "dd-mon-yyyy hh24:mi:ss"; myCtxStruct*myCtx;          if ( (myCtx = init( ctx )) == NULL ) return;     debugf( myCtx,  "Enter Pass Date" );          if ( p_idate_i == OCI_IND_NOTNULL )     {         buff_len = sizeof(buffer);         if ( OCIDateToText( myCtx->errhp, p_idate, fmt, strlen(fmt),                             NULL, -1, &buff_len, buffer ) != OCI_SUCCESS )         {             raise_application_error(myCtx,ERROR_OCI_ERROR,                                     "%s",lastOciError(myCtx));         }         else         {             debugf( myCtx,  "The date input parameter was set to '%.*s'",                     buff_len, buffer );                  if ( OCIDateAddMonths( myCtx->errhp, p_idate, 1, p_odate )                      != OCI_SUCCESS )             {                 raise_application_error(myCtx,ERROR_OCI_ERROR,                                        "%s",lastOciError(myCtx));             }             else             {                 *p_odate_i = OCI_IND_NOTNULL;                      buff_len = sizeof(buffer);                 if ( OCIDateToText( myCtx->errhp, p_odate, fmt,                                     strlen(fmt), NULL, -1,                                     &buff_len, buffer ) != OCI_SUCCESS )                 {                     raise_application_error(myCtx,ERROR_OCI_ERROR,                                             "%s",lastOciError(myCtx));                 }                 else                 {                     debugf( myCtx,                            "The date output parameter was set to '%.*s'",                             buff_len, buffer );                 }             }         }     }     term(myCtx);} 

Now we will see what is involved in passing a string back and forth. STRINGs are somewhat easier then NUMBERs and DATEs as they are passed simply as ASCII Null Terminated Strings. We will make use of the MAXLEN parameter with all OUT strings. The MAXLEN parameter tells us the maximum size of the output string buffer, which may change from call to call. This is because the caller supplies the buffer, and every time they call us, it may be with a different OUT parameter of a different length. This allows our external procedure to detect a buffer overwrite before it happens, and avoid it. We can then report back to the caller that the buffer they provided was too small (and what size it should have been).

#ifdef WIN_NT _declspec (dllexport) #endif void pass_str  ( OCIExtProcContext * ctx        /* CONTEXT */,         char *             p_istr     /* STRING */,    short              p_istr_i   /* INDICATOR short */,         char *             p_ostr     /* STRING */,    short *            p_ostr_i   /* INDICATOR short */,    int *              p_ostr_ml  /* MAXLEN int */  ) { myCtxStruct*myCtx;          if ( (myCtx = init( ctx )) == NULL ) return;     debugf( myCtx,  "Enter Pass Str" );          if ( p_istr_i == OCI_IND_NOTNULL )     {     int     l_istr_l  = strlen(p_istr);              if ( *p_ostr_ml > l_istr_l )         {                  strcpy( p_ostr, p_istr );             strupr( p_ostr );             *p_ostr_i = OCI_IND_NOTNULL;         }         else         {             raise_application_error( myCtx, ERROR_STR_TOO_SMALL,                "output buffer of %d bytes needs to be at least %d bytes",                 *p_ostr_ml, l_istr_l+1 );         }     }     term(myCtx); } 

The next routine demonstrates a binary_integer type. A binary_integer in PL/SQL is a 32-bit signed integer. It is the simplest type to send and receive by far. It is passed in a fashion that is very intuitive to a C programmer. This routine will simply inspect the input value and assign it (times 10) to the output variable:

#ifdef WIN_NT _declspec (dllexport) #endif void pass_int  ( OCIExtProcContext * ctx        /* CONTEXT */,         int                p_iINT     /* int */,    short              p_iINT_i   /* INDICATOR short */,         int *              p_oINT     /* int */,    short *            p_oINT_i   /* INDICATOR short */  ) { myCtxStruct*myCtx;          if ( (myCtx = init( ctx )) == NULL ) return;     debugf( myCtx,  "Enter Pass Int" );          if ( p_iINT_i == OCI_IND_NOTNULL )     {         debugf( myCtx,  "This first INT parameter is %d", p_iINT );              *p_oINT = p_iINT*10;         *p_oINT_i = OCI_IND_NOTNULL;              debugf( myCtx,  "Set the INT out parameter to %d", *p_oINT );     }     term(myCtx); } 

Now for a PL/SQL BOOLEAN. The PL/SQL BOOLEAN type will be mapped to a C int in this case. A value of 1 indicates true and 0 indicates false as you would expect. This routine will simply inspect the INPUT (if not Null) and set the output to the negative of the input. Again, since this maps nicely to native C types, this is very easy to code. No special environment handles or API calls to massage the data. This routine simply sets the output equal to the negation of the input:

#ifdef WIN_NT _declspec (dllexport) #endif void pass_bool  ( OCIExtProcContext * ctx        /* CONTEXT */,         int                p_ibool    /* int */,    short              p_ibool_i  /* INDICATOR short */,         int *              p_obool    /* int */,    short *            p_obool_i  /* INDICATOR short */ ) { myCtxStruct*myCtx;          if ( (myCtx = init( ctx )) == NULL ) return;     debugf( myCtx,  "Enter Pass Boolean" );          if ( p_ibool_i == OCI_IND_NOTNULL )     {         *p_obool = !p_ibool;         *p_obool_i = OCI_IND_NOTNULL;     }     term(myCtx); } 

We will now pass and return a RAW parameter. Since PL/SQL VARCHAR2 type variables are limited to 32 KB in length, we will always utilize the easier-to-interface-with RAW external type. This maps to a C unsigned char * which is just a pointer to byte data. With RAWs we will always receive the LENGTH attribute. This is mandatory; else we have no way to determine the number of bytes we should access. We will also always receive the MAXLEN attribute for all OUT parameters that have a varying length to avoid the potential of buffer overwrites. This attribute, while technically not mandatory, is just far too important to leave out. This routine simply copies the input buffer into the output buffer:

#ifdef WIN_NT _declspec (dllexport) #endif void pass_raw  ( OCIExtProcContext * ctx     /* CONTEXT */,         unsigned char *  p_iraw     /* RAW */,    short            p_iraw_i   /* INDICATOR short */,    int              p_iraw_l   /* LENGHT INT */,         unsigned char *  p_oraw     /* RAW */,    short *          p_oraw_i   /* INDICATOR short */,    int *            p_oraw_ml  /* MAXLEN int */,    int *            p_oraw_l   /* LENGTH int */  ) { myCtxStruct*myCtx;          if ( (myCtx = init( ctx )) == NULL ) return;     debugf( myCtx,  "Enter Pass long raw" );          if ( p_iraw_i == OCI_IND_NOTNULL )     {         if ( p_iraw_l <= *p_oraw_ml )         {             memcpy( p_oraw, p_iraw, p_iraw_l );                  *p_oraw_l = p_iraw_l;             *p_oraw_i = OCI_IND_NOTNULL;         }         else         {             raise_application_error( myCtx, ERROR_RAW_TOO_SMALL,                      "Buffer of %d bytes needs to be %d",                       *p_oraw_ml, p_iraw_l );         }     }     else     {         *p_oraw_i = OCI_IND_NULL;         *p_oraw_l =  0;     }     term(myCtx); } 

For our last scalar subroutine, we'll tackle LOBs. LOBs are not any harder or more complex then DATEs or NUMBERs. There are various OCI API calls that allow us to read and write them, copy them, compare them, and so on. In this example, we'll use API calls to determine the length, and then copy the input LOB to the OUTPUT LOB. This procedure requires the caller to initialize the LOB (either by selecting a LOB locator from an existing row in a table, or by utilizing dbms_lob.createtemporary). It should be noted that while we are demonstrating with a CLOB here, that the BLOB and BFILE implementations would be very similar C an OCILobLocator is used for all three types. More information on the functions you may use with the OCILobLocator type may be found in the Oracle Call Interface Programmer's Guide. This demonstration routine will simply copy the input CLOB to the output CLOB.

#ifdef WIN_NT _declspec (dllexport) #endif void pass_clob  ( OCIExtProcContext * ctx        /* CONTEXT */,         OCILobLocator *    p_iCLOB    /* OCILOBLOCATOR */,    short              p_iCLOB_i  /* INDICATOR short */,         OCILobLocator * *  p_oCLOB    /* OCILOBLOCATOR */,    short *            p_oCLOB_i  /* INDICATOR short */  ) { ub4          lob_length; myCtxStruct* myCtx;          if ( (myCtx = init( ctx )) == NULL ) return;     debugf( myCtx,  "Enter Pass Clob" );     if ( p_iCLOB_i == OCI_IND_NOTNULL && *p_oCLOB_i == OCI_IND_NOTNULL )     {         debugf( myCtx,  "both lobs are NOT NULL" );              if ( OCILobGetLength( myCtx->svchp, myCtx->errhp,                               p_iCLOB, &lob_length ) != OCI_SUCCESS )         {             raise_application_error(myCtx,ERROR_OCI_ERROR,                                     "%s",lastOciError(myCtx));         }         else         {             debugf( myCtx,  "Length of input lob was %d", lob_length );             if ( OCILobCopy(myCtx->svchp, myCtx->errhp, *p_oCLOB, p_iCLOB,                             lob_length, 1, 1) != OCI_SUCCESS )             {                 raise_application_error(myCtx,ERROR_OCI_ERROR,                                         "%s",lastOciError(myCtx));             }             else             {                      debugf( myCtx,  "We copied the lob!");             }         }     }     else     {         raise_application_error( myCtx, ERROR_CLOB_NULL,                                  "%s %s clob was NULL",                                (p_iCLOB_i == OCI_IND_NULL)?"input":"",                                (*p_oCLOB_i== OCI_IND_NULL)?"output":"" );     }     term(myCtx); } 

The following three routines demonstrate how to pass arrays of data back and forth between a stored procedure, and an external procedure. If you recall above, we created some SQL nested table types C a numArray, dateArray, and strArray. We will demonstrate with these types. In general, our routines will show how many array elements there are, dump their contents and populate the OUT array with those elements.

In these array routines, we will utilize the OCIColl* set of API calls. There are about 15 API calls we can use on collection (array) types to iterate over them, get or set their values, and so on. The ones we use below (the most common) are:

Refer to the Oracle Call Interface Programmer's Guide for an exhaustive list of available functions.

We'll start with the array of numbers. This routine will simply iterate over all of the values in the IN collection, print them out, and assign them to the OUT collection:

#ifdef WIN_NT _declspec (dllexport) #endif void pass_numArray  ( OCIExtProcContext * ctx        /* CONTEXT */,    OCIColl *           p_in       /* OCICOL  */,    short               p_in_i     /* INDICATOR short */,    OCIColl **          p_out      /* OCICOL  */,    short *             p_out_i    /* INDICATOR short */  ) { ub4        arraySize; double     tmp_dbl; boolean    exists; OCINumber *ocinum; int        i; myCtxStruct*myCtx;          if ( (myCtx = init( ctx )) == NULL ) return;     debugf( myCtx,  "Enter Pass numArray" );          if ( p_in_i == OCI_IND_NULL )     {         raise_application_error( myCtx, ERROR_ARRAY_NULL,                                        "Input array was NULL" );     }     else          if ( OCICollSize( myCtx->envhp, myCtx->errhp,                       p_in, &arraySize ) != OCI_SUCCESS )     {         raise_application_error(myCtx,ERROR_OCI_ERROR,                                 "%s",lastOciError(myCtx));     }     else     {         debugf( myCtx,  "IN Array is %d elements long", arraySize );              for( i = 0; i < arraySize; i++ )         {             if (OCICollGetElem( myCtx->envhp, myCtx->errhp, p_in, i,                               &exists, (dvoid*)&ocinum, 0 ) != OCI_SUCCESS )             {                 raise_application_error(myCtx,ERROR_OCI_ERROR,                                         "%s",lastOciError(myCtx));                 break;             }             if (OCINumberToReal( myCtx->errhp, ocinum,                                  sizeof(tmp_dbl), &tmp_dbl ) != OCI_SUCCESS )             {                 raise_application_error(myCtx,ERROR_OCI_ERROR,"%s",                                         lastOciError(myCtx));                 break;             }             debugf( myCtx,  "p_in[%d] = %g", i, tmp_dbl );             if ( OCICollAppend( myCtx->envhp, myCtx->errhp, ocinum, 0,                                 *p_out ) != OCI_SUCCESS )             {                 raise_application_error(myCtx,ERROR_OCI_ERROR,                                         "%s",lastOciError(myCtx));                 break;             }             debugf( myCtx,  "Appended to end of other array" );         }         *p_out_i = OCI_IND_NOTNULL;     }     term(myCtx); } 

Now, the next two routines do STRINGs and DATEs. They are very much similar to the number example above since they all work on OCIColl *. The strArray example is interesting in that it introduces a new OCI type C an OCIString (which is not a simple char *). We must do some double indirection with OCIString types. We'll do for strings and dates exactly what we did for numbers above:

#ifdef WIN_NT _declspec (dllexport) #endif void pass_strArray  ( OCIExtProcContext * ctx        /* CONTEXT */,    OCIColl *           p_in       /* OCICOL  */,    short               p_in_i     /* INDICATOR short */,    OCIColl **          p_out      /* OCICOL  */,    short *             p_out_i    /* INDICATOR short */  ) { ub4        arraySize; boolean    exists; OCIString * * ocistring; int        i; text      *txt; myCtxStruct*myCtx;          if ( (myCtx = init( ctx )) == NULL ) return;     debugf( myCtx,  "Enter Pass strArray" );          if ( p_in_i == OCI_IND_NULL )     {         raise_application_error( myCtx, ERROR_ARRAY_NULL,                                        "Input array was NULL" );     }     else if ( OCICollSize( myCtx->envhp, myCtx->errhp,                            p_in, &arraySize ) != OCI_SUCCESS )     {         raise_application_error(myCtx,ERROR_OCI_ERROR,                                 "%s",lastOciError(myCtx));     }     else     {         debugf( myCtx,  "IN Array is %d elements long", arraySize );         for( i = 0; i < arraySize; i++ )         {            if (OCICollGetElem( myCtx->envhp, myCtx->errhp, p_in, i, &exists,                                (dvoid*)&ocistring, 0) != OCI_SUCCESS )             {                 raise_application_error(myCtx,ERROR_OCI_ERROR,                                         "%s",lastOciError(myCtx));                 break;             }             txt = OCIStringPtr( myCtx->envhp, *ocistring );                  debugf( myCtx,  "p_in[%d] = '%s', size = %d, exists = %d",                    i, txt, OCIStringSize(myCtx->envhp,*ocistring), exists );                  if ( OCICollAppend( myCtx->envhp,myCtx->errhp, *ocistring,                                 0, *p_out ) != OCI_SUCCESS )             {                 raise_application_error(myCtx,ERROR_OCI_ERROR,                                         "%s",lastOciError(myCtx));                 break;             }             debugf( myCtx,  "Appended to end of other array" );         }         *p_out_i = OCI_IND_NOTNULL;     }     term(myCtx); }      #ifdef WIN_NT _declspec (dllexport) #endif void pass_dateArray  ( OCIExtProcContext * ctx        /* CONTEXT */,         OCIColl *           p_in       /* OCICOL  */,    short               p_in_i     /* INDICATOR short */,         OCIColl **          p_out      /* OCICOL  */,    short *             p_out_i    /* INDICATOR short */  ) { ub4        arraySize; boolean    exists; OCIDate *  ocidate; int        i; char     * fmt = "Day, Month YYYY hh24:mi:ss"; ub4        buff_len; char       buffer[255]; myCtxStruct*myCtx;          if ( (myCtx = init( ctx )) == NULL ) return;     debugf( myCtx,  "Enter Pass dateArray" );          if ( p_in_i == OCI_IND_NULL )     {         raise_application_error( myCtx, ERROR_ARRAY_NULL,                                        "Input array was NULL" );     }     else if ( OCICollSize( myCtx->envhp, myCtx->errhp,                            p_in, &arraySize ) != OCI_SUCCESS )     {         raise_application_error(myCtx,ERROR_OCI_ERROR,                                 "%s",lastOciError(myCtx));     }     else     {         debugf( myCtx,  "IN Array is %d elements long", arraySize );              for( i = 0; i < arraySize; i++ )         {             if (OCICollGetElem( myCtx->envhp, myCtx->errhp, p_in, i,                               &exists, (dvoid*)&ocidate, 0 ) != OCI_SUCCESS )             {                 raise_application_error(myCtx,ERROR_OCI_ERROR,                                         "%s",lastOciError(myCtx));                 break;             }                  buff_len = sizeof(buffer);             if ( OCIDateToText( myCtx->errhp, ocidate, fmt, strlen(fmt),                              NULL, -1, &buff_len, buffer ) != OCI_SUCCESS )             {                 raise_application_error(myCtx,ERROR_OCI_ERROR,                                         "%s",lastOciError(myCtx));                 break;             }                  debugf( myCtx,  "p_in[%d] = %.*s", i, buff_len, buffer );                  if ( OCICollAppend( myCtx->envhp,myCtx->errhp, ocidate,                                 0, *p_out ) != OCI_SUCCESS )             {                 raise_application_error(myCtx,ERROR_OCI_ERROR,                                         "%s",lastOciError(myCtx));                 break;             }             debugf( myCtx,  "Appended to end of other array" );         }         *p_out_i = OCI_IND_NOTNULL;     }     term(myCtx); } 

Lastly, we'll look at functions that return values. This looks a little unusual because in PL/SQL we simply have functions that return a value and take no inputs but the C routines we should map to must have some inputs. That is, the simplest function in PL/SQL that takes no inputs, will map to a C routine that does have formal parameters. These formal parameters will be used by the external routine to tell Oracle things such as:

So, we have seen these parameters before but they are just unexpected in a function.

#ifdef WIN_NT _declspec (dllexport) #endif OCINumber * return_number  ( OCIExtProcContext * ctx,    short *             return_i ) { double      our_number = 123.456; OCINumber * return_value; myCtxStruct*myCtx;          *return_i = OCI_IND_NULL;     if ( (myCtx = init( ctx )) == NULL ) return NULL;     debugf( myCtx,  "Enter return Number" );      

Here we must allocate storage for the number we are returning. We cannot just use a stack variable as it will go out of scope when we return. Using malloc would be a cause for a memory leak. Using a static variable would not work either as due to extproc caching, someone else can come along and alter the values we are pointing to after we return (but before Oracle has copied the value). Allocating storage is the only correct way to do this:

    return_value =             (OCINumber *)OCIExtProcAllocCallMemory(ctx, sizeof(OCINumber) );          if( return_value == NULL )     {         raise_application_error( myCtx, ERROR_OCI_ERROR,"%s","no memory" );     }     else     {         if ( OCINumberFromReal( myCtx->errhp, &our_number,                      sizeof(our_number), return_value ) != OCI_SUCCESS )         {             raise_application_error(myCtx,ERROR_OCI_ERROR,                                     "%s",lastOciError(myCtx));         }         *return_i = OCI_IND_NOTNULL;     }     term(myCtx);     return return_value; } 

Returning a date is very similar to returning a number. The same memory issues apply. We'll allocate storage for our date, fill it in, set the indicator, and return:

#ifdef WIN_NT _declspec (dllexport) #endif OCIDate * return_date  ( OCIExtProcContext * ctx,    short *             return_i ) { OCIDate * return_value; myCtxStruct*myCtx;          if ( (myCtx = init( ctx )) == NULL ) return NULL;     debugf( myCtx,  "Enter return Date" );          return_value =           (OCIDate *)OCIExtProcAllocCallMemory(ctx, sizeof(OCIDate) );          if( return_value == NULL )     {         raise_application_error( myCtx, ERROR_OCI_ERROR, "%s","no memory" );         }     else     {         *return_i = OCI_IND_NULL;         if ( OCIDateSysDate( myCtx->errhp, return_value ) != OCI_SUCCESS )         {             raise_application_error(myCtx,ERROR_OCI_ERROR,                                     "%s",lastOciError(myCtx));         }         *return_i = OCI_IND_NOTNULL;     }     term(myCtx);     return return_value; } 

With the string (the VARCHAR) return type we'll use two parameters C the indicator variable and the LENGTH field. This time, much like an OUT parameter, we set the LENGTH field to let the caller know how long the returned string is.

Many of the same considerations apply for returning strings as above; we'll allocate storage, set the indicator, supply the value, and return it:

#ifdef WIN_NT _declspec (dllexport) #endif char * return_string  ( OCIExtProcContext * ctx,    short *             return_i,    int   *             return_l ) { char * data_we_want_to_return = "Hello World!";      char * return_value; myCtxStruct*myCtx;          if ( (myCtx = init( ctx )) == NULL ) return NULL;     debugf( myCtx,  "Enter return String" );          return_value = (char *)OCIExtProcAllocCallMemory(ctx,                                         strlen(data_we_want_to_return)+1 );          if( return_value == NULL )     {         raise_application_error( myCtx, ERROR_OCI_ERROR, "%s","no memory" );     }     else     {         *return_i = OCI_IND_NULL;         strcpy( return_value, data_we_want_to_return );         *return_l = strlen(return_value);         *return_i = OCI_IND_NOTNULL;     }     term(myCtx);     return return_value; } 

This concludes the C code necessary to demonstrate the passing of all 'interesting' types as IN and IN/OUT parameters or return via functions. We were also introduced to a great number of the OCI external procedure functions such as those for storing and retrieving a context to maintain state, process parameter files, and create/write OS files. What it explicitly did not show is:

Next, we will look at makefiles that we can use to build this external procedure on either UNIX or Windows.

Building the extproc

First we will look at a general purpose makefile for Windows:

CPU=i386      MSDEV       = c:\msdev                                     (1) ORACLE_HOME = c:\oracle                                    (2)      !include <$(MSDEV)\include\win32.mak>                      (3)      TGTDLL  = extproc.dll                                      (4) OBJS    = extproc.obj                                      (5)      NTUSER32LIBS    = $(MSDEV)\lib\user32.lib   \              (6)                   $(MSDEV)\lib\msvcrt.lib  \                   $(MSDEV)\lib\oldnames.lib  \                   $(MSDEV)\lib\kernel32.lib   \                   $(MSDEV)\lib\advapi32.lib      SQLLIB  = $(ORACLE_HOME)\precomp\lib\msvc\orasql8.lib  \   (7)           $(ORACLE_HOME)\oci\lib\msvc\oci.lib      INCLS   = -I$(MSDEV)\include \                             (8)           -I$(ORACLE_HOME)\oci\include \           -I.      CFLAGS  = $(INCLS) -DWIN32 -DWIN_NT -D_DLL                 (9)      all: $(TGTDLL)                                             (10)      clean:                                                     (11)     erase *.obj *.lib *.exp      $(TGTDLL): $(OBJS)                                         (12)     $(link) -DLL $(dllflags) \         /NODEFAULTLIB:LIBC.LIB -out:$(TGTDLL) \         $(OBJS) \         $(NTUSER32LIBS) \         $(SQLLIB) 
Note 

The bold numbers in parentheses are not part of the makefile but are there simply for reference below.

  1. This is the path to where my C compiler is installed. I am using Microsoft Visual C/C++, which is the supported compiler on Windows. I will use this symbolic later in the makefile when I need to refer to this path.

  2. My ORACLE_HOME. This is used to find include files for OCI/Pro*C, and the Oracle supplied libraries.

  3. I include the standard Microsoft makefile template. This gives symbolics for things like $(link) and $(dllflags), which may change from release to release of the compiler.

  4. TGTDLL is the name of the DLL I am creating.

  5. OBJS is a list of the object files I am using for this build. If I separated the code into many files, there would be more than one obj file listed here. In this simple, small example, we have only one obj file.

  6. NTUSER32LIBS is a list of standard system libraries I am linking in.

  7. SQLLIB is a list of Oracle supplied libraries we need. In this example, I am linking in both the Pro*C and OCI libraries, although we only make use of the OCI libraries at this time. It does not hurt to include Pro*C.

  8. INCLS are the list of directories in which I have files I need to include. In here, I have the system header files, as well as the Oracle header files and the current working directory.

  9. CFLAGS is the standard C macro used by the compiler. I define CDWIN_NT to allow the conditional code we have for NT, to compile on NT (the _declspec(dllexport) for example).

  10. The all: target by default will build the DLL.

  11. The clean: target removes temporary files created during a compile.

  12. TGTDLL is the command that really creates the DLL for us. It will compile and link all of the code.

As a developer, I use and reuse this makefile constantly. Typically, I change only line (4), the output name), and line (5), the list of object code files. Other than that, the remaining components of the makefile, once configured to your system, is ready to go.

All we need do now is issue the nmake command and we should see something like:

C:\Documents and Settings\Thomas Kyte\Desktop\extproc\demo_passing>nmake      Microsoft (R) Program Maintenance Utility   Version 1.60.5270 Copyright (c) Microsoft Corp 1988-1995. All rights reserved.              cl -Ic:\msdev\include  -Ic:\oracle\oci\include  -I. -DWIN32 -DWIN_NT - D_DLL /c extproc.c Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 10.00.5270 for 80x86 Copyright (C) Microsoft Corp 1984-1995. All rights reserved.      extproc.c         link -DLL   /NODEFAULTLIB:LIBC.LIB -out:extproc.dll  extproc.obj c:\msdev\lib\user32.lib  c:\msdev\lib\msvcrt.lib   c:\msdev\lib\oldnames.lib   c:\msdev\lib\kernel32.lib c:\msdev\lib\adv api32.lib  c:\oracle\precomp\lib\msvc\orasql8.lib   c:\oracle\oci\lib\msvc\oci.lib Microsoft (R) 32-Bit Incremental Linker Version 3.00.5270 Copyright (C) Microsoft Corp 1992-1995. All rights reserved.         Creating library extproc.lib and object extproc.exp 

and that's it; our extproc.dll is built and ready to go. Now, let's port this to UNIX, using this makefile here:

MAKEFILE= $(ORACLE_HOME)/rdbms/demo/demo_rdbms.mk          (1)      INCLUDE= -I$(ORACLE_HOME)/rdbms/demo \                     (2)          -I$(ORACLE_HOME)/rdbms/public \          -I$(ORACLE_HOME)/plsql/public \          -I$(ORACLE_HOME)/network/public      TGTDLL= extproc.so                                         (3) OBJS  = extproc.o                                          (4)      all: $(TGTDLL)                                             (5)      clean:     rm *.o                                                 (6)      $(TGTDLL): $(OBJS)     $(MAKE) -f $(MAKEFILE) extproc_callback \              (7)      SHARED_LIBNAME=$(TGTDLL) OBJS=$(OBJS)      CC=cc                                                      (8) CFLAGS= -g -I. $(INCLUDE) -Wall                            (9) 
  1. Again, the bold numbers in parentheses are not part of the makefile but are there simply for reference below.

  2. The name/location of the standard Oracle makefile. I will use this makefile to cleanly compile and link with the required Oracle libraries on each platform/release. As these vary widely from release-to-release, version-to-version, and platform-to-platform, using this makefile is highly recommended.

  3. A list of directories to search for includes. I have listed the Oracle directories here.

  4. The name of the output library.

  5. All of the files that make up this library.

  6. The default target to make.

  7. A target to remove temporary files created during the make.

  8. The actual target itself. This uses the standard makefile supplied by Oracle, to build the extproc. This removes all issues with regards to library names/locations.

  9. The name of the C compiler we want to use.

  10. The standard set of options you want to pass to the C compiler.

Given the way we wrote the code, we are done with porting. All we need do is issue make and see something like the following:

$ make cc -g -I. -I/export/home/ora816/rdbms/demo -I/export/home/ora816/rdbms/public - I/export/home/ora816/plsql/public -I/export/home/ora816/network/public -Wall   -c extproc.c -o extproc.o make  -f /export/home/ora816/rdbms/demo/demo_rdbms.mk extproc_callback \  SHARED_LIBNAME=extproc.so OBJS=extproc.o make[1]: Entering directory `/aria-export/home/tkyte/src/demo_passing' ld -G -L/export/home/ora816/lib -R/export/home/ora816/lib -o extproc.so extproc.o -lclntsh  `sed -e 's/-ljava//g' /export/home/ora816/lib/ldflags`     -lnsgr8 - lnzjs8 -ln8 -lnl8 -lnro8 `sed -e 's/-ljava//g' /export/home/ora816/lib/ldflags` -lnsgr8 -lnzjs8 -ln8 -lnl8 -lclient8  -lvsn8 -lwtc8 -lcommon8 -lgeneric8 -lwtc8 - lmm -lnls8  -lcore8 -lnls8 -lcore8 -lnls8  `sed -e 's/-ljava//g' /export/home/ora816/lib/ldflags`     -lnsgr8 -lnzjs8 -ln8 -lnl8 -lnro8 `sed -e  's/-ljava//g' /export/home/ora816/lib/ldflags`     -lnsgr8 -lnzjs8 -ln8 -lnl8 - lclient8  -lvsn8 -lwtc8 -lcommon8 -lgeneric8  -ltrace8 -lnls8  -lcore8 -lnls8 - lcore8 -lnls8  -lclient8  -lvsn8 -lwtc8 -lcommon8 -lgeneric8 -lnls8  -lcore8 -lnls8 -lcore8 -lnls8    `cat /export/home/ora816/lib/sysliblist` `if [ -f /usr/lib/libsched.so ] ; then echo -lsched ; else true; fi`  - R/export/home/ora816/lib -laio  -lposix4 -lkstat -lm  -lthread \ /export/home/ora816/lib/libpls8.a make[1]: Leaving directory `/aria-export/home/tkyte/src/demo_passing' 

then we have our extproc.so file for Solaris.

Installing and Running

Now that we have our call specification, the create library, create types, and demo_passing package spec and body in extproc.sql, and extproc.dll (or extproc.so), we are ready to install the example into the database. To do so, we will simply execute @extproc.sql and then run a series of anonymous blocks to exercise our external procedure. You will need to customize the CREATE LIBRARY statement to point to your .dll or .so:

create or replace library demoPassing as 'C:\<LOCATION OF YOUR DLL>\extproc.dll'; 

but the rest should compile 'as is'.

So, after we run extproc.sql, we will test our external procedures as such:

SQL> declare   2      l_input    number;   3      l_output number;   4  begin   5      dbms_output.put_line( 'Pass Number' );   6   7      dbms_output.put_line('first test passing nulls to see that works');   8      demo_passing_pkg.pass( l_input, l_output );   9      dbms_output.put_line( 'l_input = '||l_input||                                        ' l_output = '||l_output );  10  11      l_input := 123;  12      dbms_output.put_line            ( 'Now test passing non-nulls to see that works' );  13      dbms_output.put_line( 'We expect the output to be -123' );  14      demo_passing_pkg.pass( l_input, l_output );  15      dbms_output.put_line            ( 'l_input = '||l_input||' l_output = '||l_output );  16  end;  17  / Pass Number first test passing nulls to see that works l_input =  l_output = Now test passing non-nulls to see that works We expect the output to be -123 l_input = 123 l_output = -123      PL/SQL procedure successfully completed. 

I have a simple anonymous block to test each procedure/function in turn. We won't embed the output from each one from here on. Rather, there is a test_all.sql script included in the example code that exercises each procedure/function, and produces output similar to the above. You may execute this after installing the demo to see each one working.

Now, if you remember in the C code, we had a series of debugf statements. If, before executing the above block of PL/SQL, I simply make sure an ext_proc.log file exists in my temporary directory, we can see the output of debugf. It will look like this:

000809 185056 GMT (    extproc.c,176) Enter Pass Number 000809 185056 GMT (    extproc.c,183) Oci Environment Retrieved 000809 185056 GMT (    extproc.c,176) Enter Pass Number 000809 185056 GMT (    extproc.c,183) Oci Environment Retrieved 000809 185056 GMT (    extproc.c,209) The first parameter is 123 000809 185056 GMT (    extproc.c,230) Set OUT parameter to -123 and set indicator to NOTNULL 

This shows that on August 9, 2000 (000809) at 6:50:56PM (185056) GMT, the source code file extproc.c executed on line 176, the debugf statement that said Enter Pass Number. It goes on to record the rest of the debugf statements we executed. As you can see, having a trace file that we can turn on and off at will could be very handy in a debugging situation. Since external procedures run on the server, they can be notoriously hard to debug. While the opportunity to use a conventional debugger does exist, the practicality of that option is very limited.

LOB to File External Procedure (LOB_IO)

Oracle 8.0 introduced a set of new data types:

Using a CLOB or BLOB, I can store upto 4 GBs of unstructured data in the database. Using a BFILE I can access OS files residing on the database server's filesystem in a read-only fashion. Oracle supplies a DBMS_LOB package with many utility routines to manipulate a LOB. It even provides a function to loadfromfile for loading a LOB from an existing OS file. What Oracle does not provide however, is a function to write a LOB to an OS file. UTL_FILE could be used for the CLOB type in many cases, but it could never be used for the BLOB type. We will implement a routine now using an external procedure written in C with Pro*C that permits us to write all CLOBs and BLOBs to a file.

The LOB_IO Call Specification

Again, we will start with a CREATE LIBRARY statement, then define our package specification, then a package body to map to the C routines, and finally, we'll implement our C routine using Pro*C. Starting with the library specification we have:

tkyte@TKYTE816> create or replace library lobToFile_lib   2  as 'C:\extproc\lobtofile\extproc.dll'   3  / Library created. 

And then the specificiation of the package we are creating. It starts with three overloaded functions to write a LOB to a file on our server. They are called in identical fashion and all return the number of bytes written to disk. The exceptions they might throw are listed below them.

tkyte@TKYTE816> create or replace package lob_io   2  as   3   4      function write( p_path in varchar2,   5                      p_filename in varchar2, p_lob in blob )   6      return binary_integer;   7   8      function write( p_path in varchar2,   9                      p_filename in varchar2, p_lob in clob )  10      return binary_integer;  11  12      function write( p_path in varchar2,  13                      p_filename in varchar2, p_lob in bfile )  14      return binary_integer;  15  16      IO_ERROR exception;  17      pragma exception_init( IO_ERROR, -20001 );  18  19      CONNECT_ERROR exception;  20      pragma exception_init( CONNECT_ERROR, -20002 );  21  22      INVALID_LOB exception;  23      pragma exception_init( INVALID_LOB, -20003 );  24  25      INVALID_FILENAME exception;  26      pragma exception_init( INVALID_FILENAME, -20004 );  27  28      OPEN_FILE_ERROR exception;  29      pragma exception_init( OPEN_FILE_ERROR, -20005 );  30  31      LOB_READ_ERROR exception;  32      pragma exception_init( LOB_READ_ERROR, -20006 );  33  34  end;  35  / Package created. 

Here we have taken every error code we might raise (the #define ERROR_ codes we define at the top of our extproc below) and have mapped them to named exceptions in PL/SQL. This is a nice touch that allows the user of our package to either catch a named exception like this:

exception     when lob_io.IO_ERROR then             ...     when lob_io.CONNECT_ERROR then             ... 

or if they prefer, error codes and error messages instead of named exceptions like this:

exception     when others then         if (sqlcode = -20001 ) then -- (it was an IO error)             ...        elsif( sqlcode = -20002 )  then -- (it was a connect error)               and so on 

It is also a nice way to determine exactly what errors might possible by raised by the external procedure without having to inspect the C code directly.

Now for the package body - this simply maps the PL/SQL specification from above to the C routine in our lobToFile library:

tkyte@TKYTE816> create or replace package body lob_io   2  as   3   4  function write(p_path in varchar2,p_filename in varchar2,p_lob in blob)   5  return binary_integer   6  as   7  language C name "lobToFile" library lobtofile_lib   8  with context parameters ( CONTEXT,   9    p_path     STRING,        p_path     INDICATOR short,  10    p_filename STRING,        p_filename INDICATOR short,  11    p_lob      OCILOBLOCATOR, p_lob      INDICATOR short,  12    RETURN INDICATOR short );  13  14  15  function write(p_path in varchar2,p_filename in varchar2,p_lob in clob)  16  return binary_integer  17  as  18  language C name "lobToFile" library lobtofile_lib  19  with context parameters ( CONTEXT,  20    p_path     STRING,        p_path     INDICATOR short,  21    p_filename STRING,        p_filename INDICATOR short,  22    p_lob      OCILOBLOCATOR, p_lob      INDICATOR short,  23    RETURN INDICATOR short );  24  25  26  function write(p_path in varchar2,p_filename in varchar2,                                                          p_lob in bfile)  27  return binary_integer  28  as  29  language C name "lobToFile" library lobtofile_lib  30  with context parameters ( CONTEXT,  31    p_path     STRING,        p_path     INDICATOR short,  32    p_filename STRING,        p_filename INDICATOR short,  33    p_lob      OCILOBLOCATOR, p_lob      INDICATOR short,  34    RETURN INDICATOR short );  35  36  end lob_io;  37  /      Package body created. 

It is somewhat interesting to note that all three functions map to the same exact external C function. I did not write a separate routine for CLOBs, BLOBs, and BFILEs. Since the LOB is passed as an OCILOBLOCATOR, they can all use the same routine. As usual, I am passing an indicator variable for each formal parameter, and one for the return value. While not mandatory, they are strongly encouraged.

The LOB_IO Pro*C Code

Now we'll inspect the Pro*C code we will generate to implement the lobtofile_lib library. I'm leaving the generic code we discussed in the first example out for brevity (the implementation of debugf, raise_application_error, ociLastError, term, and init are the same, with the exception that we use EXEC SQL REGISTER CONNECT in Pro*C applications in the init function) and will jump right into the code itself. It should be noted that the following code goes at the end of our 'template' code from above and that the relevant sections regarding connections with Pro*C would be uncommented in the template. We start with all of the errors we will possibly return. This set of error codes should match exactly with the named exceptions and their SQLCodes in our PL/SQL package specification. There is nothing to ensure that this is the case as this is purely a convention I use, but it is a good practice.

#define ERROR_FWRITE              20001 #define ERROR_REGISTER_CONNECT    20002 #define ERROR_BLOB_IS_NULL        20003 #define ERROR_FILENAME_IS_NULL    20004 #define ERROR_OPEN_FILE           20005 #define ERROR_LOB_READ            20006 

Next comes an internal routine, not available to PL/SQL directly, which will be used by the main lobToFile routine to write bytes to a file. It also keeps a running total of the number of bytes written to the file:

static int writeToFile( myCtxStruct *       myCtx,                         OCIFileObject *     output,                         char *              buff,                         int                 bytes,                         int *               totalWritten ) { ub4    bytesWritten;          debugf( myCtx, "Writing %d bytes to output", bytes );     if ( OCIFileWrite( myCtx->envhp, myCtx->errhp, output,                        buff, bytes, &bytesWritten ) != OCI_SUCCESS )     {         return raise_application_error                ( myCtx,                  ERROR_FWRITE,                  "Error writing to file '%s'",                  lastOciError(myCtx) );     }          if ( bytesWritten != bytes )     {         return raise_application_error                ( myCtx,                  ERROR_FWRITE,                  "Error writing %d bytes to file, only %d written",                   bytes, bytesWritten );     }     *totalWritten += bytesWritten;     return 0; } 

The first parameter to this routine is our session context. This context must be passed down to any routine so that we can call utilities such as raise_application_error. The next parameter is the output file we will be writing to. We are using the portable OCIFile functions to perform our I/O. It is expected that this file be already opened prior to calling writeToFile. Following that are the pointers to the buffer to write to, along with the number of bytes it currently is pointing to. Lastly is a counter variable we are using to keep a running total with.

Now for the main, (and the last) routine. This routine does all of the real work; it takes a LOB locator as an input (any of BLOB, CLOB, or BFILE) and writes the contents to the named file:

#ifdef WIN_NT _declspec (dllexport) #endif int lobToFile( OCIExtProcContext * ctx,                char *              path, short               path_i,                char *              filename,                short               filename_i,                OCIBlobLocator  *   blob,                short               blob_i,                short *             return_indicator ) { 

This next part of the code defines the structure we will fetch into. It contains a leading byte count and then 64 KB of data space. We'll fetch 64 KB at a time from the LOB and write it to disk. It then goes onto define some other local variables we need:

typedef struct long_varraw {   ub4  len;   text buf[65536]; } long_varraw;      EXEC SQL TYPE long_varraw IS LONG VARRAW(65536);      long_varraw    data;    /* we'll fetch into this */ ub4            amt;     /* this will be how much was fetched */ ub4            buffsize = sizeof(data.buf);  /* this is the amt we ask for*/ int            offset = 1; /* where in the lob we are currently reading */ OCIFileObject* output = NULL; /* file we write to */ int            bytesWritten = 0; /* how many bytes we WROTE in total */ myCtxStruct *  myCtx;          *return_indicator = OCI_IND_NULL;     if ( (myCtx=init(ctx)) == NULL )  return 0; 

We begin by inspecting the Null indicators. If either is set, we must fail the request. This points out the importance of always passing an indicator to an external procedure in C. You never know when the end user of your code will slip you a Null by accident. If we attempt to access filename or BLOB without checking first and they are Null - we may very well 'crash' (our extproc will crash) as they are not initialized.

    if ( blob_i == OCI_IND_NULL )     {         raise_application_error                ( myCtx,                  ERROR_BLOB_IS_NULL,                  "Null lob passed to lobToFile, invalid argument" );     }     else if ( filename_i == OCI_IND_NULL || path_i == OCI_IND_NULL )     {         raise_application_error                ( myCtx,                  ERROR_FILENAME_IS_NULL,                 "Null Filename/path passed to lobToFile, invalid argument");     } 

Now, open the output file. We open with the intent to 'write' in 'binary' mode We just want to dump bytes from the database to a file.

    else if ( OCIFileOpen( myCtx->envhp, myCtx->errhp, &output,                       filename, path,                       OCI_FILE_WRITE_ONLY, OCI_FILE_CREATE,                       OCI_FILE_BIN ) != OCI_SUCCESS )     {         raise_application_error( myCtx,                                  ERROR_OPEN_FILE,                                 "Error opening file '%s'",                                  lastOciError(myCtx) );     }     else     {         debugf( myCtx, "lobToFile( filename => '%s%s', lob => %X )",                  path, filename, blob ); 

We will now read the LOB using Pro*C in a non-polling method. This is important, as you cannot 'poll' for a LOB in an external procedure. Hence, we will never ask for more than we can receive in one call (non-polling). We start at offset 1 (the first byte) and will read BUFSIZE (64 KB in this case) bytes at a time. Every time through, we'll increment our offset by the amount we just read and we'll exit the loop when the amount read is less then the amount requested - indicating we have read the entire BLOB;

        for( offset = 1, amt = buffsize;               amt == buffsize;              offset += amt )         {             debugf( myCtx, "Attempt to read %d bytes from LOB", amt );             EXEC SQL LOB                       READ :amt                       FROM :blob                       AT   :offset                       INTO :data                       WITH LENGTH :buffsize; 

Check for any and all errors, we'll convert that into our error message and add the real error message onto the PL/SQL error stack.

Note how we are careful to clean up any and all resources (the open file) before we return. This is important. You do not want to 'leak' resources if possible. We do this by returning from only one location (below) and calling term before we do so:

            if ( sqlca.sqlcode < 0 )                 break;                  if ( writeToFile(myCtx, output, data.buf, amt, &bytesWritten) )                 break;         }     } 

All we need do now is close the file and return:

    if ( output != NULL )     {         debugf( myCtx, "Done and closing file" );         OCIFileClose( myCtx->envhp, myCtx->errhp, output );     }          *return_indicator = OCI_IND_NOTNULL;     debugf( myCtx, "Returning a value of %d for total bytes written",             bytesWritten );          term( myCtx );     return bytesWritten; } 

Building the extproc

The process for building lobtofile is virtually identical as it was for the demo_passing library above. The generic makefile was utilized on both Windows and UNIX with minimal modification. On Windows we use:

CPU=i386      MSDEV       = c:\msdev ORACLE_HOME = c:\oracle      !include <$(MSDEV)\include\win32.mak>      TGTDLL = extproc.dll OBJS   = lobtofile.obj      NTUSER32LIBS    = $(MSDEV)\lib\user32.lib   \                   $(MSDEV)\lib\msvcrt.lib  \                   $(MSDEV)\lib\oldnames.lib  \                   $(MSDEV)\lib\kernel32.lib   \                   $(MSDEV)\lib\advapi32.lib      SQLLIB  = $(ORACLE_HOME)\precomp\lib\msvc\orasql8.lib  \           $(ORACLE_HOME)\oci\lib\msvc\oci.lib      INCLS   = -I$(MSDEV)\include \           -I$(ORACLE_HOME)\oci\include \           -I.      CFLAGS  = $(INCLS) -DWIN32 -DWIN_NT -D_DLL      all: $(TGTDLL)      clean:     erase *.obj *.lib *.exp lobtofile.c      $(TGTDLL): $(OBJS)     $(link) -DLL $(dllflags) \         /NODEFAULTLIB:LIBC.LIB -out:$(TGTDLL) \         $(OBJS) \         $(NTUSER32LIBS) \         $(SQLLIB) \      lobtofile.c: lobtofile.pc         proc \          include=$(ORACLE_HOME)\network\public \          include=$(ORACLE_HOME)\proc\lib \          include=$(ORACLE_HOME)\rdbms\demo \          include=$(ORACLE_HOME)\oci\include \          include=$(MSDEV) \include \          lines=yes \          parse=full \          iname=lobtofile.pc 

The only alterations are in bold font. We changed the name of the OBJ files we are linking in and we added the rule to convert lobtofile.pc into lobtofile.c for us. We simply invoke the command line Pro*C precompiler and tell it where our include files are (INCLUDE=), that we would like line numbers preserved in our .c file (lines=yes), that we would like to take full advantage of its ability to understand C (parse=full), and that the input filename to convert is lobtofile.pc (iname=). Now all we need do is issue nmake and our DLL will be built.

On UNIX, the makefile is:

MAKEFILE= $(ORACLE_HOME)/rdbms/demo/demo_rdbms.mk      INCLUDE= -I$(ORACLE_HOME)/rdbms/demo \          -I$(ORACLE_HOME)/rdbms/public \          -I$(ORACLE_HOME)/plsql/public \          -I$(ORACLE_HOME)/network/public      TGTDLL= extproc.so OBJS  = lobtofile.o           all: $(TGTDLL)      clean:     rm *.o           lobtofile.c: lobtofile.pc     proc  \          include=$(ORACLE_HOME)/network/public \          include=$(ORACLE_HOME)/proc/lib \          include=$(ORACLE_HOME)/rdbms/demo \          include=$(ORACLE_HOME)/rdbms/public \          lines=yes \          iname=lobtofile.pc      extproc.so: lobtofile.c lobtofile.o     $(MAKE) -f $(MAKEFILE) extproc_callback \      SHARED_LIBNAME=extproc.so OBJS="lobtofile.o"      CC=cc CFLAGS= -g -I. $(INCLUDE) 

Again, the same exact alterations we made on Windows, we make on UNIX. We simply add the Pro*C precompile command and change the name of the object code we are linking. Type make and we have our .so file.

Now we are ready to test and use it.

Installing and Using LOB_IO

All we need to do now is run our CREATE LIBRARY, CREATE PACKAGE, and CREATE PACKAGE BODY. This installs the LOB_IO package into our database. To test it, we will use a couple of anonymous PL/SQL blocks. The first block we will execute exercises our error detection and handling. We will call our external procedure and deliberately pass it bad inputs, bad directory names, and the like. Here it is with comments explaining what we are expecting to see at each step:

SQL> REM for NT SQL> REM define PATH=c:\temp\ SQL> REM define CMD=fc /b      SQL> REM for UNIX SQL> define PATH=/tmp/ SQL> define CMD="diff -s"      SQL> drop table demo; Table dropped.      SQL> create table demo( theBlob blob, theClob clob ); Table created.      SQL> /* DOC> * the following block tests all of the error conditions we DOC> * can test for. It does not test for IO_ERROR (we'd need a full DOC> * disk or something for that) or CONNECT_ERROR (that should *never* DOC> * happen) DOC> */ SQL> SQL> declare   2      l_blob    blob;   3      l_bytes number;   4  begin   5   6      /*   7       * Try a NULL blob   8       */   9      begin  10          l_bytes := lob_io.write( '&PATH', 'test.dat', l_blob );  11      exception  12          when lob_io.INVALID_LOB then  13              dbms_output.put_line( 'invalid arg caught as expected' );  14              dbms_output.put_line( rpad('-',70,'-') );  15      end;  16  17      /*  18       * Now, we'll try with a real blob and a NULL filename  19       */  20      begin  21          insert into demo (theBlob) values( empty_blob() )  22          returning theBlob into l_blob;  23  24          l_bytes := lob_io.write( NULL, NULL, l_blob );  25      exception  26          when lob_io.INVALID_FILENAME then  27              dbms_output.put_line( 'invalid arg caught as expected again' );  28              dbms_output.put_line( rpad('-',70,'-') );  29      end;  30  31      /*  32       * Now, try with an OK blob but a directory that does not exist  33       */  34      begin  35          l_bytes := lob_io.write( '/nonexistent/directory', 'x.dat', l_blob );  36      exception  37          when lob_io.OPEN_FILE_ERROR then  38              dbms_output.put_line( 'caught open file error expected' );  39              dbms_output.put_line( sqlerrm );  40              dbms_output.put_line( rpad('-',70,'-') );  41      end;  42  43      /*  44       * Lets just try writing it out to see that work  45       */  46      l_bytes := lob_io.write( '&PATH', '1.dat', l_blob );  47      dbms_output.put_line( 'Writing successful ' || l_bytes || ' bytes' );  48      dbms_output.put_line( rpad('-',70,'-') );  49  50      rollback;  51  52      /*  53       * Now we have a non-null blob BUT we rolled back so its an  54       * invalid lob locator.  Lets see what our extproc returns  55       * now...  56       */  57      begin  58          l_bytes := lob_io.write( '&PATH', '1.dat', l_blob );  59      exception  60          when lob_io.LOB_READ_ERROR then  61              dbms_output.put_line( 'caught lob read error expected' );  62              dbms_output.put_line( sqlerrm );  63              dbms_output.put_line( rpad('-',70,'-') );  64      end;  65  end;  66  / old  10:         l_bytes := lob_io.write( '&PATH', 'test.dat', l_blob ); new  10:         l_bytes := lob_io.write( '/tmp/', 'test.dat', l_blob ); old  46:     l_bytes := lob_io.write( '&PATH', '1.dat', l_blob ); new  46:     l_bytes := lob_io.write( '/tmp/', '1.dat', l_blob ); old  58:         l_bytes := lob_io.write( '&PATH', '1.dat', l_blob ); new  58:         l_bytes := lob_io.write( '/tmp/', '1.dat', l_blob );      invalid arg caught as expected ---------------------------------------------------------------------- invalid arg caught as expected again ---------------------------------------------------------------------- caught open file error expected ORA-20005: Error opening file 'ORA-30152: File does not exist' ---------------------------------------------------------------------- Writing successful 0 bytes ----------------------------------------------------------------------      PL/SQL procedure successfully completed. 

As you can see, everything happened as expected. We forced many errors to happen and they happened exactly as planned. Now, let's use our package as it was intended. In order to test this, I'll create a directory object mapped to my temporary directory (/tmp on UNIX, C:\temp\ on Windows). A directory object is used by BFILEs to permit reading of files in a given directory. In the OS filesystem (/tmp or C:\temp\) I'll place a file to test with called something.big. This is just a fairly large file to test with. Its contents are not relevant. We will load this file into a CLOB, then a BLOB, and finally a temporary BLOB. We'll use our routine to write each of these to a separate file. We'll finish up by using OS utilities (diff on UNIX, FC on Windows) to compare the generated files with the original input file:

SQL> create or replace directory my_files as '&PATH.'; old   1: create or replace directory my_files as '&PATH.' new   1: create or replace directory my_files as '/tmp/'      Directory created.      SQL> SQL> declare   2      l_blob    blob;   3      l_clob    clob;   4      l_bfile    bfile;   5  begin   6      insert into demo   7      values ( empty_blob(), empty_clob() )   8      returning theBlob, theClob into l_blob, l_clob;   9  10      l_bfile := bfilename ( 'MY_FILES', 'something.big' );  11  12      dbms_lob.fileopen( l_bfile );  13  14      dbms_lob.loadfromfile( l_blob, l_bfile,  15                             dbms_lob.getlength( l_bfile ) );  16  17      dbms_lob.loadfromfile( l_clob, l_bfile,  18                             dbms_lob.getlength( l_bfile ) );  19  20      dbms_lob.fileclose( l_bfile );  21      commit;  22  end;  23  /      PL/SQL procedure successfully completed. 

So, this has now loaded the file something.big into our database, once into a BLOB and then into a CLOB data element. Now, we'll write them back out:

SQL> declare   2      l_bytes number;   3      l_bfile    bfile;   4  begin   5      for x in ( select theBlob from demo )   6      loop   7          l_bytes := lob_io.write( '&PATH','blob.dat', x.theBlob );   8          dbms_output.put_line( 'Wrote ' || l_bytes ||' bytes of blob' );   9      end loop;  10  11      for x in ( select theClob from demo )  12      loop  13          l_bytes := lob_io.write( '&PATH','clob.dat', x.theclob );  14          dbms_output.put_line( 'Wrote ' || l_bytes ||' bytes of clob' );  15      end loop;  16  17      l_bfile := bfilename ( 'MY_FILES', 'something.big' );  18      dbms_lob.fileopen( l_bfile );  19      l_bytes := lob_io.write( '&PATH','bfile.dat', l_bfile );  20      dbms_output.put_line( 'Wrote ' || l_bytes || ' bytes of bfile' );  21      dbms_lob.fileclose( l_bfile );  22  end;  23  / old   7:         l_bytes := lob_io.write( '&PATH','blob.dat', x.theBlob ); new   7:         l_bytes := lob_io.write( '/tmp/','blob.dat', x.theBlob ); old  13:         l_bytes := lob_io.write( '&PATH','clob.dat', x.theclob ); new  13:         l_bytes := lob_io.write( '/tmp/','clob.dat', x.theclob ); old  19:     l_bytes := lob_io.write( '&PATH','bfile.dat', l_bfile ); new  19:     l_bytes := lob_io.write( '/tmp/','bfile.dat', l_bfile ); Wrote 1107317 bytes of blob Wrote 1107317 bytes of clob Wrote 1107317 bytes of bfile      PL/SQL procedure successfully completed. 

This shows that we successfully called our external procedure, and wrote the file out three times. Each time it was the same exact size (as expected). Now, we'll create a temporary LOB, copy the file into it, and write that out, just to make sure we can work with temporary LOBs as well:

SQL> declare   2      l_tmpblob blob;   3      l_blob    blob;   4      l_bytes      number;   5  begin   6    select theBlob into l_blob from demo;   7   8    dbms_lob.createtemporary(l_tmpblob,TRUE);   9  10    dbms_lob.copy(l_tmpblob,l_blob,dbms_lob.getlength(l_blob),1,1);  11  12    l_bytes := lob_io.write( '&PATH','tempblob.dat', l_tmpblob );  13    dbms_output.put_line( 'Wrote ' || l_bytes ||' bytes of temp_blob' );  14       15      DBMS_LOB.FREETEMPORARY(l_tmpblob);  16  END;  17  / old  12:     l_bytes := lob_io.write( '&PATH','tempblob.dat', l_tmpblob ); new  12:     l_bytes := lob_io.write( '/tmp/','tempblob.dat', l_tmpblob ); Wrote 1107317 bytes of temp_blob      PL/SQL procedure successfully completed. 

So, that was successful and, fortunately, it wrote the same number of bytes out for us. The last step is to use the OS utilities to verify the files we just wrote out are the same as what we loaded:

SQL> host &CMD &PATH.something.big &PATH.blob.dat Files /tmp/something.big and /tmp/blob.dat are identical      SQL> host &CMD &PATH.something.big &PATH.clob.dat Files /tmp/something.big and /tmp/clob.dat are identical      SQL> host &CMD &PATH.something.big &PATH.bfile.dat Files /tmp/something.big and /tmp/bfile.dat are identical      SQL> host &CMD &PATH.something.big &PATH.tempblob.dat Files /tmp/something.big and /tmp/tempblob.dat are identical 

and that concludes this new feature; LOB_IO.

Errors You May Encounter

The following is a list of common errors you may encounter when using external procedures. Some of them we have talked about already, for example, the error you'll get if your listener or TNSNAMES.ORA file is not configured correctly, but many we have not. We'll look at them now, explaining when they would happen, and what you can do to correct them.

All of these errors are documented in the Oracle 8i Error Messages Manual as well.

ORA-28575 "unable to open RPC connection to external procedure agent"

28575, 00000, "unable to open RPC connection to external procedure agent" // *Cause:   Initialization of a network connection to the extproc agent did //           not succeed. This problem can be caused by network problems, //           incorrect listener configuration, or incorrect transfer code. // *Action:  Check listener configuration in LISTENER.ORA and TNSNAMES.ORA, or //           check Oracle Names Server. 

This error almost always indicates an incorrectly configured TNSNAMES.ORA or LISTENER.ORA. We covered the possible causes and solutions for this error earlier in the Configuring Your Server section.

ORA-28576 "lost RPC connection to external procedure agent"

28576, 00000, "lost RPC connection to external procedure agent" // *Cause:  A fatal error occurred in either an RPC network connection, //          the extproc agent, or the invoked 3GL after communication had //          been established successfully. // *Action: First check the 3GL code you are invoking; the most likely //          cause of this error is abnormal termination of the //          invoked "C" routine. If this is not the case, check for //          network problems. Correct the problem if you find it. If all //          components appear to be normal but the problem persists, the //          problem could be an internal logic error in the RPC transfer //          code.  Contact your customer support representative. // 

This error, when reported against an external procedure you have written, almost certainly implies a bug in your developed code. This error occurs when the external process 'disappears'. This will happen if your program 'crashes'. For example, I added:

char * return_string  ( OCIExtProcContext * ctx,    short *             return_i,    int   *             return_l ) {       *return_i = OCI_IND_NOTNULL;     *(char*)NULL = 1;     return return_value; } 

to the bottom of my return_string example. After a recompile, I find the following:

ops$tkyte@ORA816.US.ORACLE.COM> exec dbms_output.put_line(  demo_passing_pkg.return_string ) BEGIN dbms_output.put_line( demo_passing_pkg.return_string ); END;      * ERROR at line 1: ORA-28576: lost RPC connection to external procedure agent 

will always happen (until I debug and fix the code, of course).

ORA-28577 "argument %s of external procedure %s has unsupported datatype %s"

28577, 00000, "argument %s of external procedure %s has unsupported datatype %s" // *Cause:  While transferring external procedure arguments to the agent, //          an unsupported datatype was detected. // *Action: Check your documentation for the supported datatypes of external //          procedure arguments. 

This error will occur if you try to pass a data type from PL/SQL to an external procedure that is not supported by this interface. In particular, an example of this would be a PL/SQL table type. If, in the demo_passing example, we had declared the numArray type in the package spec as:

    ...     type numArray is table of number index by binary_integer;          procedure pass( p_in in numArray, p_out out numArray );     ... 

instead of as a SQL nested table type as we did, we would find at run-time that:

  1  declare   2      l_input   demo_passing_pkg.numArray;   3      l_output  demo_passing_pkg.numArray;   4  begin   5      demo_passing_pkg.pass( l_input, l_output );   6* end; SQL> / declare * ERROR at line 1: ORA-28577: argument 2 of external procedure pass_numArray has unsupported datatype ORA-06512: at "OPS$TKYTE.DEMO_PASSING_PKG", line 0 ORA-06512: at line 5 

This is because the passing of PL/SQL table types is not supported (we can pass collections, but not PL/SQL table types).

ORA-28578 "protocol error during callback from an external procedure"

28578, 00000, "protocol error during callback from an external procedure" // *Cause:  An internal protocol error occurred while trying to execute a //          callback to the Oracle server from the user's 3GL routine. // *Action: Contact Oracle customer support. 

Hopefully, we never see this error and the one shown below. It would indicate an internal error within Oracle. The only thing to do upon receiving this error is to attempt to reproduce it with a very small testcase and report it to Oracle support.

ORA-28579 "network error during callback from external procedure agent"

ORA-28579 "network error during callback from external procedure agent" // *Cause:  An internal network error occurred while trying to execute a //          callback to the Oracle server from the user's 3GL routine. // *Action: Contact Oracle customer support. 

ORA-28580 "recursive external procedures are not supported"

// *Cause:  A callback from within a user's 3GL routine resulted in the //          invocation of another external procedure. // *Action: Make sure that the SQL code executed in a callback does not directly //          call another external procedure, or indirectly results in another //          external procedure, such as triggers calling external //          procedures, PL/SQL procedures calling external procedures, etc. 

This error will occur when you do a callback from an external procedure into the database, and the procedure you call performs another callout to another external procedure. In short, an external procedure cannot directly, or indirectly, call another external procedure. We can demonstrate this by modifying our LOB_IO .pc file. In this file, I added:

    { int x;          exec sql execute begin         :x := demo_passing_pkg.return_number;     end; end-exec;          if ( sqlca.sqlcode < 0 )     {         return raise_application_error                ( ctx,                  20000,                 "Error:\n%.70s",                  sqlca.sqlerrm.sqlerrmc );     }     } 

right after the REGISTER CONNECT call. Now, whenever I execute lob_io, we will receive:

ops$tkyte@DEV8I.WORLD> declare x clob; y number; begin y := lob_io.write( 'x', x ); end;   2  / declare x clob; y number; begin y := lob_io.write( 'x', x ); end; * ERROR at line 1: ORA-20000: Error: ORA-28580: recursive external procedures are not supported ORA-06512: ORA-06512: at "OPS$TKYTE.LOB_IO", line 0 ORA-06512: at line 1 

The only solution is never to call another external procedure from an external procedure.

ORA-28582 "a direct connection to this agent is not allowed"

$ oerr ora 28582 28582, 00000, "a direct connection to this agent is not allowed" // *Cause:  A user or a tool tried to establish a direct connection to either //       an external procedure agent or a Heterogeneous Services agent, //       for example: "SVRMGR> CONNECT SCOTT/TIGER@NETWORK_ALIAS".This type //       of connection is not allowed. //*Action: When executing the CONNECT statement, make sure your database link //       or network alias is not pointing to a Heterogeneous Option agent or //          an external procedure agent. 

We should never see this error. It will only happen when you attempt to connect to a database, and accidently use a service name that is configured to connect to an extproc service.

ORA-06520 "PL/SQL: Error loading external library"

$ oerr ora 6520 06520, 00000, "PL/SQL: Error loading external library" // *Cause:  An error was detected by PL/SQL trying to load the external //          library dynamically. // *Action: Check the stacked error (if any) for more details. 

This error should be followed up immediately by an OS-specific error. For example, to see this error I simply do the following:

$ cp lobtofile.pc extproc.so 

I copied my source code over my .so file and that is defintely going to cause problems! Now when I run the external procedure, I receive:

declare x clob; y number; begin y := lob_io.write( 'x', x ); end; * ERROR at line 1: ORA-06520: PL/SQL: Error loading external library ORA-06522: ld.so.1: extprocPLSExtProc: fatal: /export/home/tkyte/src/lobtofile/extproc.so: unknown file type ORA-06512: at "OPS$TKYTE.LOB_IO", line 0 ORA-06512: at line 1 

So, as you can see, the error stack contains the OS error which tells us this is an unknown type of file, and this will help you diagnose this error (in this case, it is easy to diagnose - extproc.so is actually some C code).

ORA-06521 "PL/SQL: Error mapping function"

$ oerr ora 6521 06521, 00000, "PL/SQL: Error mapping function" // *Cause:  An error was detected by PL/SQL trying to map the mentioned //          function dynamically. // *Action: Check the stacked error (if any) for more details. 

This error is typically the result of one of these two things:

To see this error, I modified the lobtofile.pc source code to have:

#ifdef WIN_NT _declspec (dllexport) #endif int xlobToFile( OCIExtProcContext * ctx,                char *              filename, 

I added an x to the filename. Now when we run this, we receive:

declare x clob; y number; begin y := lob_io.write( 'x', x ); end; * ERROR at line 1: ORA-06521: PL/SQL: Error mapping function ORA-06522: ld.so.1: extprocPLSExtProc: fatal: lobToFile: can't find symbol ORA-06512: at "OPS$TKYTE.LOB_IO", line 0 ORA-06512: at line 1 

This shows that the error is can't find symbol, meaning we have a mismatch between the name name in the PL/SQL wrapper, and the name of the function in the external library. Either we have a typo, or we've forgotten to export the name (on Windows).

ORA-06523 "Maximum number of arguments exceeded"

$ oerr ora 6523 06523, 00000, "Maximum number of arguments exceeded" // *Cause:  There is an upper limit on the number of arguments that one //          can pass to the external function. // *Action: Check the port specific documentation on how to calculate the //          upper limit. 

You will get this error if you have an unusually large parameter list. The number of elements that may be passed to an external procedure is about 128 (less if you pass doubles as they take 8 bytes, not 4). If you get this error, and you really need to send that many inputs, the easiest work-around is to use a collection type. For example:

  1  declare   2      l_input   strArray := strArray();   3      l_output  strArray := strArray();   4  begin   5      dbms_output.put_line( 'Pass strArray' );   6      for i in 1 .. 1000 loop   7          l_input.extend;   8          l_input(i) := 'Element ' || i;   9      end loop;  10      demo_passing_pkg.pass( l_input, l_output );  11      dbms_output.put_line( 'l_input.count = ' || l_input.count ||  12                            ' l_output.count = ' || l_output.count );  13      for i in 1 .. l_input.count loop  14          if ( l_input(i) != l_output(i) ) then  15              raise program_error;  16          end if;  17      end loop;  18* end; SQL> / Pass strArray l_input.count = 1000 l_output.count = 1000      PL/SQL procedure successfully completed. 

shows that I can send 1,000 strings, many times the limit on the number of scalars, to an external procedure via a collection.

ORA-06525 "Length Mismatch for CHAR or RAW data"

06525, 00000, "Length Mismatch for CHAR or RAW data" // *Cause:  The length specified in the length variable has an illegal //          value. This can happen if you have requested requested a PL/SQL //          INOUT, OUT or RETURN raw variable to be passed as a RAW with //          no corresponding length variable. This error can also happen //          if there is a mismatch in the length value set in the length //          variable and the length in the orlvstr or orlraw. // // *Action: Correct the external procedure code and set the length variable //          correctly. 

This error, if you follow my usage for sending and returning parameters, will only occur on the RAW type and a string that is returned from a function. The solution is very straightforward - you must set the length correctly. For a Null RAW OUT parameter, the length must be set to 0, as I did in the examples above. For a non Null RAW OUT parameter, the length must be set to some value less than or equal to MAXLEN. Likewise, for a string that we will return, the length must be set correctly (to less than MAXLEN but since you are responsible for setting the storage for the string, there is no MAXLEN so LENGTH must be less than or equal to 32760, which is the largest PL/SQL can handle).

ORA-06526 "Unable to load PL/SQL library"

$ oerr ora 6526 06526, 00000, "Unable to load PL/SQL library" // *Cause:  PL/SQL was unable to instantiate the library referenced by this //          referenced in the EXTERNAL syntax. This is a serious error and //          should normally not happen. // // *Action: Report this problem to customer support. 

This is an internal error. We should not see this, but if it appears, there are two things that can happen:

Firstly, this error can be accompanied by some other error with some more details. It might look like this:

ERROR at line 1: ORA-6526: Unable to load PL/SQL library ORA-4030: out of process memory when trying to allocate 65036 bytes (callheap,KQL tmpbuf) 

This one is self-explanatory - we've run out of memory. We need to reduce the amount of memory we are using elsewhere.

Alternatively, the error message that accompanies the ORA-6526 doesn't lead us to any positive conclusions. In this case, we must contact support.

ORA-06527 "External procedure SQLLIB error: %s"

$ oerr ora 6527 06527, 00000, "External procedure SQLLIB error: %s" // *Cause:  An error occurred in sqllib during execution of a Pro* external //          procedure. // // *Action: The message text indicates the actual SQLLIB error that //          occurred.  Consult the Oracle Error Messages and Codes manual //          for a complete description of the error message and follow //          the appropriate action. 

This one is self-explanatory. The error message will contain more information.

Summary

In this chapter, we have covered the main issues surrounding external procedures such as:

Given the generic template and makefiles above, you have all you need to write an external procedure from start to finish in a couple of minutes now. The tricky part is mapping the datatypes and the tables above, but that is easily accomplished by following the two tables in The Wrapper section - they tell you 'given this type, you will use that type'. Then, just follow the guidelines I have for passing the parameters in the example above (always send the context, always send the MAXLEN attribute for strings and raws, always send the Null indicator, and so on). If you do that, you'll be writing external procedures in no time.



Expert One on One Oracle
Full Frontal PR: Getting People Talking about You, Your Business, or Your Product
ISBN: 1590595254
EAN: 2147483647
Year: 2005
Pages: 41
Authors: Richard Laermer, Michael Prichinello
BUY ON AMAZON

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net