File: api-design.rst

package info (click to toggle)
odc 1.4.6-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 3,060 kB
  • sloc: cpp: 22,074; f90: 3,707; sh: 999; ansic: 471; python: 382; makefile: 39
file content (255 lines) | stat: -rw-r--r-- 8,272 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
.. index:: API Design

API Design
==========


.. index:: API Design; C Interface

.. _`c-interface`:

C Interface
-----------

The C API interface is designed to have a consistent approach to function call design, and to error and argument handling.


Calling Convention
~~~~~~~~~~~~~~~~~~

The library is built around a collection of objects which handle functionality. As C does not expose objects directly, the API uses pointers to opaque types (such as ``odc_frame_t``). Objects are allocated internally and these pointers provide handles to the objects. Methods called against these objects are presented as C functions of the form ``odc_<object-type>_<method-name>(<opaque-handle>, ...)``.

All functions return an integer return code, with the sole exception of ``odc_error_string`` which obtains the details of a previous error. This is beneficial for :ref:`error-handling` as it makes it possible to wrap every function in the same way.

A side effect of this design choice is that all inputs and outputs are passed as arguments to functions. All output values are passed as pointers, and many functions accept a null pointers for optional output values.

.. code-block:: c

   long row_count;

   int rc = odc_frame_row_count(frame, &row_count);


.. _`error-handling`:

Error Handling
~~~~~~~~~~~~~~

All functions return a status code, which should be checked. In case of error a human readable message can be obtained using ``odc_error_string``.

The return code is always one of the following:

``ODC_SUCCESS``
   The function completed successfully.

``ODC_ITERATION_COMPLETE``
   All frames have been returned, and the loop can be terminated successfully.

``ODC_ERROR_GENERAL_EXCEPTION``
   A known error was encountered. Call ``odc_error_string()`` with the returned code for details.

``ODC_ERROR_UNKNOWN_EXCEPTION``
   An unexpected and unrecognised error was encountered. Call ``odc_error_string()`` with the returned code for details.


.. code-block:: c

   int rc = odc_new_frame(&frame, reader);

   if (rc != ODC_SUCCESS) {
       // Error, retrieve message and print it.
       fprintf(stderr, "Failed to construct frame: %s\n", odc_error_string(rc));
   }
   else {
       // Success, continue processing.
   }


.. note::

   Internally, **odc** is written in C++ and the error handling uses exceptions. All exceptions will be caught on the C/C++ boundary in the C API and an appropriate error code will be returned.


To facilitate consistent error handling, it may be useful to define wrapper functions or macros to handle the error checking. As a trivial example,

.. code-block:: c

   #define CHECK_RESULT(x) \
       do { \
           int rc = (x); \
           if (rc != ODC_SUCCESS) { \
               fprintf(stderr, "Error calling odc function \"%s\": %s\n", #x, odc_error_string(rc)); \
               exit(1); \
           } \
       } while (false); \

   long row_count;
   int column_count;

   CHECK_RESULT(odc_frame_row_count(frame, &row_count));
   CHECK_RESULT(odc_frame_column_count(frame, &column_count));


Failure Handler
~~~~~~~~~~~~~~~

In certain scenarios, it might be more appropriate to have a callback on error. Instead of checking return code after each call, a handler function can be set that will be called back after an error has occurred.

This approach is very useful when a specific clean-up procedure is needed, before current process is aborted.

.. code-block:: c

   void handle_failure(void* context, int error_code) {
       fprintf(stderr, "Error: %s\n", odc_error_string(error_code));
       clean_up();
       exit(1);
   }

   odc_set_failure_handler(handle_failure, NULL);


The ``context`` parameter is user-specified, and is defined as the second argument to ``odc_set_failure_handler``.


.. index:: API Design; Fortran Interface

Fortran Interface
-----------------

The Fortran interface wraps the :ref:`C functions <c-interface>`, with a number of practical differences.


Calling Convention
~~~~~~~~~~~~~~~~~~

Unlike C, Fortran supports custom types. As such, the objects referenced in the API are presented as Fortran objects with the appropriate types. The appropriate function calls are thus methods on these type instances.

All functions return a status code that should be checked for error conditions. The standard Fortran mechanism is used to support optional arguments.

.. code-block:: fortran

   type(odc_reader) :: reader
   type(odc_frame) :: frame
   logical, parameter :: aggregated = .true.
   integer(8), parameter :: max_aggregated_rows = 1000000
   integer(8), target :: row_count

   rc = frame%initialise(reader)
   rc = frame%next(aggregated, max_aggregated_rows)
   rc = frame%row_count(row_count)


Error Handling
~~~~~~~~~~~~~~

All functions return a status code, which should be checked. In case of error a human readable message can be obtained using ``odc_error_string``.

The return code is always one of the following:

``ODC_SUCCESS``
   The function completed successfully.

``ODC_ITERATION_COMPLETE``
   All frames have been returned, and the loop can be terminated successfully.

``ODC_ERROR_GENERAL_EXCEPTION``
   A known error was encountered. Call ``odc_error_string()`` with the returned code for details.

``ODC_ERROR_UNKNOWN_EXCEPTION``
   An unexpected and unrecognised error was encountered. Call ``odc_error_string()`` with the returned code for details.


.. code-block:: fortran

   rc = frame%initialise(reader)

   if (rc /= ODC_SUCCESS) then
       ! Error, retrieve message and print it.
       print *, "Failed to construct frame: ", odc_error_string(rc)
   else
       ! Success, continue processing.
   end if


To facilitate consistent error handling, it may be useful to define a wrapper function for checking the return codes in a consistent manner.

.. code-block:: fortran

   integer(8), target :: row_count
   integer, target :: column_count

   call check_call(frame%row_count(row_count))
   call check_call(frame%column_count(column_count))


.. code-block:: fortran

   subroutine check_call(rc)
       integer, intent(in) :: rc

       if (rc /= ODC_SUCCESS) then
           print *, "Error: ", odc_error_string(err)
           stop 1
       end if
   end subroutine


Failure Handler
~~~~~~~~~~~~~~~

In certain scenarios, it might be more appropriate to have a callback on error. Instead of checking return code after each call, a handler function can be set that will be called back after an error has occurred.

This approach is very useful when a specific clean-up procedure is needed, before current process is aborted.

.. code-block:: fortran

   integer(8), parameter :: context = 123456
   rc = odc_set_failure_handler(error_handler, context)


.. code-block:: fortran

   subroutine error_handler(context, error)
         integer(8), intent(in) :: context
         integer, intent(in) :: error

         print *, "Custom error handler"
         print *, "Error: ", odc_error_string(error)
         print *, "Context: ", context
         stop 1
   end subroutine


The ``context`` parameter is under user control, and is defined as the second argument to ``odc_set_failure_handler``.


Optional Parameters
~~~~~~~~~~~~~~~~~~~

Many API functions take optional parameters, especially for returning (selected) attributes about **Frames** or other objects. These parameters may be omitted as indicated in :doc:`the API Reference </content/reference/f90-reference>`.

The two calls below can be considered identical.

.. code-block:: fortran

   logical, parameter :: aggregated = .true.
   integer(8), parameter :: max_aggregated_rows = 1000000

   err = frame%next(aggregated, max_aggregated_rows)

   ! since aggregated defaults to true anyway, we can skip it and define only maximum_rows
   err = frame%next(maximum_rows=max_aggregated_rows)


.. index:: API Design; C++ Interface
   :name: cpp-interface

C++ Interface
-------------

The interface in C++ mainly exists as an underlying base for implementing :ref:`the C API <c-interface>` which wraps it. It is only suitable to be used within an environment in which `eckit`_ is being used. If this is not the case it’s recommended to use the C API.

All C++ functions will throw an exception in case of error.


.. _`eckit`: https://github.com/ecmwf/eckit