1*e71b7053SJung-uk Kim=pod 2*e71b7053SJung-uk Kim 3*e71b7053SJung-uk Kim=head1 NAME 4*e71b7053SJung-uk Kim 5*e71b7053SJung-uk KimASYNC_get_wait_ctx, 6*e71b7053SJung-uk KimASYNC_init_thread, ASYNC_cleanup_thread, ASYNC_start_job, ASYNC_pause_job, 7*e71b7053SJung-uk KimASYNC_get_current_job, ASYNC_block_pause, ASYNC_unblock_pause, ASYNC_is_capable 8*e71b7053SJung-uk Kim- asynchronous job management functions 9*e71b7053SJung-uk Kim 10*e71b7053SJung-uk Kim=head1 SYNOPSIS 11*e71b7053SJung-uk Kim 12*e71b7053SJung-uk Kim #include <openssl/async.h> 13*e71b7053SJung-uk Kim 14*e71b7053SJung-uk Kim int ASYNC_init_thread(size_t max_size, size_t init_size); 15*e71b7053SJung-uk Kim void ASYNC_cleanup_thread(void); 16*e71b7053SJung-uk Kim 17*e71b7053SJung-uk Kim int ASYNC_start_job(ASYNC_JOB **job, ASYNC_WAIT_CTX *ctx, int *ret, 18*e71b7053SJung-uk Kim int (*func)(void *), void *args, size_t size); 19*e71b7053SJung-uk Kim int ASYNC_pause_job(void); 20*e71b7053SJung-uk Kim 21*e71b7053SJung-uk Kim ASYNC_JOB *ASYNC_get_current_job(void); 22*e71b7053SJung-uk Kim ASYNC_WAIT_CTX *ASYNC_get_wait_ctx(ASYNC_JOB *job); 23*e71b7053SJung-uk Kim void ASYNC_block_pause(void); 24*e71b7053SJung-uk Kim void ASYNC_unblock_pause(void); 25*e71b7053SJung-uk Kim 26*e71b7053SJung-uk Kim int ASYNC_is_capable(void); 27*e71b7053SJung-uk Kim 28*e71b7053SJung-uk Kim=head1 DESCRIPTION 29*e71b7053SJung-uk Kim 30*e71b7053SJung-uk KimOpenSSL implements asynchronous capabilities through an ASYNC_JOB. This 31*e71b7053SJung-uk Kimrepresents code that can be started and executes until some event occurs. At 32*e71b7053SJung-uk Kimthat point the code can be paused and control returns to user code until some 33*e71b7053SJung-uk Kimsubsequent event indicates that the job can be resumed. 34*e71b7053SJung-uk Kim 35*e71b7053SJung-uk KimThe creation of an ASYNC_JOB is a relatively expensive operation. Therefore, for 36*e71b7053SJung-uk Kimefficiency reasons, jobs can be created up front and reused many times. They are 37*e71b7053SJung-uk Kimheld in a pool until they are needed, at which point they are removed from the 38*e71b7053SJung-uk Kimpool, used, and then returned to the pool when the job completes. If the user 39*e71b7053SJung-uk Kimapplication is multi-threaded, then ASYNC_init_thread() may be called for each 40*e71b7053SJung-uk Kimthread that will initiate asynchronous jobs. Before 41*e71b7053SJung-uk Kimuser code exits per-thread resources need to be cleaned up. This will normally 42*e71b7053SJung-uk Kimoccur automatically (see L<OPENSSL_init_crypto(3)>) but may be explicitly 43*e71b7053SJung-uk Kiminitiated by using ASYNC_cleanup_thread(). No asynchronous jobs must be 44*e71b7053SJung-uk Kimoutstanding for the thread when ASYNC_cleanup_thread() is called. Failing to 45*e71b7053SJung-uk Kimensure this will result in memory leaks. 46*e71b7053SJung-uk Kim 47*e71b7053SJung-uk KimThe B<max_size> argument limits the number of ASYNC_JOBs that will be held in 48*e71b7053SJung-uk Kimthe pool. If B<max_size> is set to 0 then no upper limit is set. When an 49*e71b7053SJung-uk KimASYNC_JOB is needed but there are none available in the pool already then one 50*e71b7053SJung-uk Kimwill be automatically created, as long as the total of ASYNC_JOBs managed by the 51*e71b7053SJung-uk Kimpool does not exceed B<max_size>. When the pool is first initialised 52*e71b7053SJung-uk KimB<init_size> ASYNC_JOBs will be created immediately. If ASYNC_init_thread() is 53*e71b7053SJung-uk Kimnot called before the pool is first used then it will be called automatically 54*e71b7053SJung-uk Kimwith a B<max_size> of 0 (no upper limit) and an B<init_size> of 0 (no ASYNC_JOBs 55*e71b7053SJung-uk Kimcreated up front). 56*e71b7053SJung-uk Kim 57*e71b7053SJung-uk KimAn asynchronous job is started by calling the ASYNC_start_job() function. 58*e71b7053SJung-uk KimInitially B<*job> should be NULL. B<ctx> should point to an ASYNC_WAIT_CTX 59*e71b7053SJung-uk Kimobject created through the L<ASYNC_WAIT_CTX_new(3)> function. B<ret> should 60*e71b7053SJung-uk Kimpoint to a location where the return value of the asynchronous function should 61*e71b7053SJung-uk Kimbe stored on completion of the job. B<func> represents the function that should 62*e71b7053SJung-uk Kimbe started asynchronously. The data pointed to by B<args> and of size B<size> 63*e71b7053SJung-uk Kimwill be copied and then passed as an argument to B<func> when the job starts. 64*e71b7053SJung-uk KimASYNC_start_job will return one of the following values: 65*e71b7053SJung-uk Kim 66*e71b7053SJung-uk Kim=over 4 67*e71b7053SJung-uk Kim 68*e71b7053SJung-uk Kim=item B<ASYNC_ERR> 69*e71b7053SJung-uk Kim 70*e71b7053SJung-uk KimAn error occurred trying to start the job. Check the OpenSSL error queue (e.g. 71*e71b7053SJung-uk Kimsee L<ERR_print_errors(3)>) for more details. 72*e71b7053SJung-uk Kim 73*e71b7053SJung-uk Kim=item B<ASYNC_NO_JOBS> 74*e71b7053SJung-uk Kim 75*e71b7053SJung-uk KimThere are no jobs currently available in the pool. This call can be retried 76*e71b7053SJung-uk Kimagain at a later time. 77*e71b7053SJung-uk Kim 78*e71b7053SJung-uk Kim=item B<ASYNC_PAUSE> 79*e71b7053SJung-uk Kim 80*e71b7053SJung-uk KimThe job was successfully started but was "paused" before it completed (see 81*e71b7053SJung-uk KimASYNC_pause_job() below). A handle to the job is placed in B<*job>. Other work 82*e71b7053SJung-uk Kimcan be performed (if desired) and the job restarted at a later time. To restart 83*e71b7053SJung-uk Kima job call ASYNC_start_job() again passing the job handle in B<*job>. The 84*e71b7053SJung-uk KimB<func>, B<args> and B<size> parameters will be ignored when restarting a job. 85*e71b7053SJung-uk KimWhen restarting a job ASYNC_start_job() B<must> be called from the same thread 86*e71b7053SJung-uk Kimthat the job was originally started from. 87*e71b7053SJung-uk Kim 88*e71b7053SJung-uk Kim=item B<ASYNC_FINISH> 89*e71b7053SJung-uk Kim 90*e71b7053SJung-uk KimThe job completed. B<*job> will be NULL and the return value from B<func> will 91*e71b7053SJung-uk Kimbe placed in B<*ret>. 92*e71b7053SJung-uk Kim 93*e71b7053SJung-uk Kim=back 94*e71b7053SJung-uk Kim 95*e71b7053SJung-uk KimAt any one time there can be a maximum of one job actively running per thread 96*e71b7053SJung-uk Kim(you can have many that are paused). ASYNC_get_current_job() can be used to get 97*e71b7053SJung-uk Kima pointer to the currently executing ASYNC_JOB. If no job is currently executing 98*e71b7053SJung-uk Kimthen this will return NULL. 99*e71b7053SJung-uk Kim 100*e71b7053SJung-uk KimIf executing within the context of a job (i.e. having been called directly or 101*e71b7053SJung-uk Kimindirectly by the function "func" passed as an argument to ASYNC_start_job()) 102*e71b7053SJung-uk Kimthen ASYNC_pause_job() will immediately return control to the calling 103*e71b7053SJung-uk Kimapplication with ASYNC_PAUSE returned from the ASYNC_start_job() call. A 104*e71b7053SJung-uk Kimsubsequent call to ASYNC_start_job passing in the relevant ASYNC_JOB in the 105*e71b7053SJung-uk KimB<*job> parameter will resume execution from the ASYNC_pause_job() call. If 106*e71b7053SJung-uk KimASYNC_pause_job() is called whilst not within the context of a job then no 107*e71b7053SJung-uk Kimaction is taken and ASYNC_pause_job() returns immediately. 108*e71b7053SJung-uk Kim 109*e71b7053SJung-uk KimASYNC_get_wait_ctx() can be used to get a pointer to the ASYNC_WAIT_CTX 110*e71b7053SJung-uk Kimfor the B<job>. ASYNC_WAIT_CTXs can have a "wait" file descriptor associated 111*e71b7053SJung-uk Kimwith them. Applications can wait for the file descriptor to be ready for "read" 112*e71b7053SJung-uk Kimusing a system function call such as select or poll (being ready for "read" 113*e71b7053SJung-uk Kimindicates that the job should be resumed). If no file descriptor is made 114*e71b7053SJung-uk Kimavailable then an application will have to periodically "poll" the job by 115*e71b7053SJung-uk Kimattempting to restart it to see if it is ready to continue. 116*e71b7053SJung-uk Kim 117*e71b7053SJung-uk KimAn example of typical usage might be an async capable engine. User code would 118*e71b7053SJung-uk Kiminitiate cryptographic operations. The engine would initiate those operations 119*e71b7053SJung-uk Kimasynchronously and then call L<ASYNC_WAIT_CTX_set_wait_fd(3)> followed by 120*e71b7053SJung-uk KimASYNC_pause_job() to return control to the user code. The user code can then 121*e71b7053SJung-uk Kimperform other tasks or wait for the job to be ready by calling "select" or other 122*e71b7053SJung-uk Kimsimilar function on the wait file descriptor. The engine can signal to the user 123*e71b7053SJung-uk Kimcode that the job should be resumed by making the wait file descriptor 124*e71b7053SJung-uk Kim"readable". Once resumed the engine should clear the wake signal on the wait 125*e71b7053SJung-uk Kimfile descriptor. 126*e71b7053SJung-uk Kim 127*e71b7053SJung-uk KimThe ASYNC_block_pause() function will prevent the currently active job from 128*e71b7053SJung-uk Kimpausing. The block will remain in place until a subsequent call to 129*e71b7053SJung-uk KimASYNC_unblock_pause(). These functions can be nested, e.g. if you call 130*e71b7053SJung-uk KimASYNC_block_pause() twice then you must call ASYNC_unblock_pause() twice in 131*e71b7053SJung-uk Kimorder to re-enable pausing. If these functions are called while there is no 132*e71b7053SJung-uk Kimcurrently active job then they have no effect. This functionality can be useful 133*e71b7053SJung-uk Kimto avoid deadlock scenarios. For example during the execution of an ASYNC_JOB an 134*e71b7053SJung-uk Kimapplication acquires a lock. It then calls some cryptographic function which 135*e71b7053SJung-uk Kiminvokes ASYNC_pause_job(). This returns control back to the code that created 136*e71b7053SJung-uk Kimthe ASYNC_JOB. If that code then attempts to acquire the same lock before 137*e71b7053SJung-uk Kimresuming the original job then a deadlock can occur. By calling 138*e71b7053SJung-uk KimASYNC_block_pause() immediately after acquiring the lock and 139*e71b7053SJung-uk KimASYNC_unblock_pause() immediately before releasing it then this situation cannot 140*e71b7053SJung-uk Kimoccur. 141*e71b7053SJung-uk Kim 142*e71b7053SJung-uk KimSome platforms cannot support async operations. The ASYNC_is_capable() function 143*e71b7053SJung-uk Kimcan be used to detect whether the current platform is async capable or not. 144*e71b7053SJung-uk Kim 145*e71b7053SJung-uk Kim=head1 RETURN VALUES 146*e71b7053SJung-uk Kim 147*e71b7053SJung-uk KimASYNC_init_thread returns 1 on success or 0 otherwise. 148*e71b7053SJung-uk Kim 149*e71b7053SJung-uk KimASYNC_start_job returns one of ASYNC_ERR, ASYNC_NO_JOBS, ASYNC_PAUSE or 150*e71b7053SJung-uk KimASYNC_FINISH as described above. 151*e71b7053SJung-uk Kim 152*e71b7053SJung-uk KimASYNC_pause_job returns 0 if an error occurred or 1 on success. If called when 153*e71b7053SJung-uk Kimnot within the context of an ASYNC_JOB then this is counted as success so 1 is 154*e71b7053SJung-uk Kimreturned. 155*e71b7053SJung-uk Kim 156*e71b7053SJung-uk KimASYNC_get_current_job returns a pointer to the currently executing ASYNC_JOB or 157*e71b7053SJung-uk KimNULL if not within the context of a job. 158*e71b7053SJung-uk Kim 159*e71b7053SJung-uk KimASYNC_get_wait_ctx() returns a pointer to the ASYNC_WAIT_CTX for the job. 160*e71b7053SJung-uk Kim 161*e71b7053SJung-uk KimASYNC_is_capable() returns 1 if the current platform is async capable or 0 162*e71b7053SJung-uk Kimotherwise. 163*e71b7053SJung-uk Kim 164*e71b7053SJung-uk Kim=head1 NOTES 165*e71b7053SJung-uk Kim 166*e71b7053SJung-uk KimOn Windows platforms the openssl/async.h header is dependent on some 167*e71b7053SJung-uk Kimof the types customarily made available by including windows.h. The 168*e71b7053SJung-uk Kimapplication developer is likely to require control over when the latter 169*e71b7053SJung-uk Kimis included, commonly as one of the first included headers. Therefore 170*e71b7053SJung-uk Kimit is defined as an application developer's responsibility to include 171*e71b7053SJung-uk Kimwindows.h prior to async.h. 172*e71b7053SJung-uk Kim 173*e71b7053SJung-uk Kim=head1 EXAMPLE 174*e71b7053SJung-uk Kim 175*e71b7053SJung-uk KimThe following example demonstrates how to use most of the core async APIs: 176*e71b7053SJung-uk Kim 177*e71b7053SJung-uk Kim #ifdef _WIN32 178*e71b7053SJung-uk Kim # include <windows.h> 179*e71b7053SJung-uk Kim #endif 180*e71b7053SJung-uk Kim #include <stdio.h> 181*e71b7053SJung-uk Kim #include <unistd.h> 182*e71b7053SJung-uk Kim #include <openssl/async.h> 183*e71b7053SJung-uk Kim #include <openssl/crypto.h> 184*e71b7053SJung-uk Kim 185*e71b7053SJung-uk Kim int unique = 0; 186*e71b7053SJung-uk Kim 187*e71b7053SJung-uk Kim void cleanup(ASYNC_WAIT_CTX *ctx, const void *key, OSSL_ASYNC_FD r, void *vw) 188*e71b7053SJung-uk Kim { 189*e71b7053SJung-uk Kim OSSL_ASYNC_FD *w = (OSSL_ASYNC_FD *)vw; 190*e71b7053SJung-uk Kim 191*e71b7053SJung-uk Kim close(r); 192*e71b7053SJung-uk Kim close(*w); 193*e71b7053SJung-uk Kim OPENSSL_free(w); 194*e71b7053SJung-uk Kim } 195*e71b7053SJung-uk Kim 196*e71b7053SJung-uk Kim int jobfunc(void *arg) 197*e71b7053SJung-uk Kim { 198*e71b7053SJung-uk Kim ASYNC_JOB *currjob; 199*e71b7053SJung-uk Kim unsigned char *msg; 200*e71b7053SJung-uk Kim int pipefds[2] = {0, 0}; 201*e71b7053SJung-uk Kim OSSL_ASYNC_FD *wptr; 202*e71b7053SJung-uk Kim char buf = 'X'; 203*e71b7053SJung-uk Kim 204*e71b7053SJung-uk Kim currjob = ASYNC_get_current_job(); 205*e71b7053SJung-uk Kim if (currjob != NULL) { 206*e71b7053SJung-uk Kim printf("Executing within a job\n"); 207*e71b7053SJung-uk Kim } else { 208*e71b7053SJung-uk Kim printf("Not executing within a job - should not happen\n"); 209*e71b7053SJung-uk Kim return 0; 210*e71b7053SJung-uk Kim } 211*e71b7053SJung-uk Kim 212*e71b7053SJung-uk Kim msg = (unsigned char *)arg; 213*e71b7053SJung-uk Kim printf("Passed in message is: %s\n", msg); 214*e71b7053SJung-uk Kim 215*e71b7053SJung-uk Kim if (pipe(pipefds) != 0) { 216*e71b7053SJung-uk Kim printf("Failed to create pipe\n"); 217*e71b7053SJung-uk Kim return 0; 218*e71b7053SJung-uk Kim } 219*e71b7053SJung-uk Kim wptr = OPENSSL_malloc(sizeof(OSSL_ASYNC_FD)); 220*e71b7053SJung-uk Kim if (wptr == NULL) { 221*e71b7053SJung-uk Kim printf("Failed to malloc\n"); 222*e71b7053SJung-uk Kim return 0; 223*e71b7053SJung-uk Kim } 224*e71b7053SJung-uk Kim *wptr = pipefds[1]; 225*e71b7053SJung-uk Kim ASYNC_WAIT_CTX_set_wait_fd(ASYNC_get_wait_ctx(currjob), &unique, 226*e71b7053SJung-uk Kim pipefds[0], wptr, cleanup); 227*e71b7053SJung-uk Kim 228*e71b7053SJung-uk Kim /* 229*e71b7053SJung-uk Kim * Normally some external event would cause this to happen at some 230*e71b7053SJung-uk Kim * later point - but we do it here for demo purposes, i.e. 231*e71b7053SJung-uk Kim * immediately signalling that the job is ready to be woken up after 232*e71b7053SJung-uk Kim * we return to main via ASYNC_pause_job(). 233*e71b7053SJung-uk Kim */ 234*e71b7053SJung-uk Kim write(pipefds[1], &buf, 1); 235*e71b7053SJung-uk Kim 236*e71b7053SJung-uk Kim /* Return control back to main */ 237*e71b7053SJung-uk Kim ASYNC_pause_job(); 238*e71b7053SJung-uk Kim 239*e71b7053SJung-uk Kim /* Clear the wake signal */ 240*e71b7053SJung-uk Kim read(pipefds[0], &buf, 1); 241*e71b7053SJung-uk Kim 242*e71b7053SJung-uk Kim printf ("Resumed the job after a pause\n"); 243*e71b7053SJung-uk Kim 244*e71b7053SJung-uk Kim return 1; 245*e71b7053SJung-uk Kim } 246*e71b7053SJung-uk Kim 247*e71b7053SJung-uk Kim int main(void) 248*e71b7053SJung-uk Kim { 249*e71b7053SJung-uk Kim ASYNC_JOB *job = NULL; 250*e71b7053SJung-uk Kim ASYNC_WAIT_CTX *ctx = NULL; 251*e71b7053SJung-uk Kim int ret; 252*e71b7053SJung-uk Kim OSSL_ASYNC_FD waitfd; 253*e71b7053SJung-uk Kim fd_set waitfdset; 254*e71b7053SJung-uk Kim size_t numfds; 255*e71b7053SJung-uk Kim unsigned char msg[13] = "Hello world!"; 256*e71b7053SJung-uk Kim 257*e71b7053SJung-uk Kim printf("Starting...\n"); 258*e71b7053SJung-uk Kim 259*e71b7053SJung-uk Kim ctx = ASYNC_WAIT_CTX_new(); 260*e71b7053SJung-uk Kim if (ctx == NULL) { 261*e71b7053SJung-uk Kim printf("Failed to create ASYNC_WAIT_CTX\n"); 262*e71b7053SJung-uk Kim abort(); 263*e71b7053SJung-uk Kim } 264*e71b7053SJung-uk Kim 265*e71b7053SJung-uk Kim for (;;) { 266*e71b7053SJung-uk Kim switch (ASYNC_start_job(&job, ctx, &ret, jobfunc, msg, sizeof(msg))) { 267*e71b7053SJung-uk Kim case ASYNC_ERR: 268*e71b7053SJung-uk Kim case ASYNC_NO_JOBS: 269*e71b7053SJung-uk Kim printf("An error occurred\n"); 270*e71b7053SJung-uk Kim goto end; 271*e71b7053SJung-uk Kim case ASYNC_PAUSE: 272*e71b7053SJung-uk Kim printf("Job was paused\n"); 273*e71b7053SJung-uk Kim break; 274*e71b7053SJung-uk Kim case ASYNC_FINISH: 275*e71b7053SJung-uk Kim printf("Job finished with return value %d\n", ret); 276*e71b7053SJung-uk Kim goto end; 277*e71b7053SJung-uk Kim } 278*e71b7053SJung-uk Kim 279*e71b7053SJung-uk Kim /* Wait for the job to be woken */ 280*e71b7053SJung-uk Kim printf("Waiting for the job to be woken up\n"); 281*e71b7053SJung-uk Kim 282*e71b7053SJung-uk Kim if (!ASYNC_WAIT_CTX_get_all_fds(ctx, NULL, &numfds) 283*e71b7053SJung-uk Kim || numfds > 1) { 284*e71b7053SJung-uk Kim printf("Unexpected number of fds\n"); 285*e71b7053SJung-uk Kim abort(); 286*e71b7053SJung-uk Kim } 287*e71b7053SJung-uk Kim ASYNC_WAIT_CTX_get_all_fds(ctx, &waitfd, &numfds); 288*e71b7053SJung-uk Kim FD_ZERO(&waitfdset); 289*e71b7053SJung-uk Kim FD_SET(waitfd, &waitfdset); 290*e71b7053SJung-uk Kim select(waitfd + 1, &waitfdset, NULL, NULL, NULL); 291*e71b7053SJung-uk Kim } 292*e71b7053SJung-uk Kim 293*e71b7053SJung-uk Kim end: 294*e71b7053SJung-uk Kim ASYNC_WAIT_CTX_free(ctx); 295*e71b7053SJung-uk Kim printf("Finishing\n"); 296*e71b7053SJung-uk Kim 297*e71b7053SJung-uk Kim return 0; 298*e71b7053SJung-uk Kim } 299*e71b7053SJung-uk Kim 300*e71b7053SJung-uk KimThe expected output from executing the above example program is: 301*e71b7053SJung-uk Kim 302*e71b7053SJung-uk Kim Starting... 303*e71b7053SJung-uk Kim Executing within a job 304*e71b7053SJung-uk Kim Passed in message is: Hello world! 305*e71b7053SJung-uk Kim Job was paused 306*e71b7053SJung-uk Kim Waiting for the job to be woken up 307*e71b7053SJung-uk Kim Resumed the job after a pause 308*e71b7053SJung-uk Kim Job finished with return value 1 309*e71b7053SJung-uk Kim Finishing 310*e71b7053SJung-uk Kim 311*e71b7053SJung-uk Kim=head1 SEE ALSO 312*e71b7053SJung-uk Kim 313*e71b7053SJung-uk KimL<crypto(7)>, L<ERR_print_errors(3)> 314*e71b7053SJung-uk Kim 315*e71b7053SJung-uk Kim=head1 HISTORY 316*e71b7053SJung-uk Kim 317*e71b7053SJung-uk KimASYNC_init_thread, ASYNC_cleanup_thread, 318*e71b7053SJung-uk KimASYNC_start_job, ASYNC_pause_job, ASYNC_get_current_job, ASYNC_get_wait_ctx(), 319*e71b7053SJung-uk KimASYNC_block_pause(), ASYNC_unblock_pause() and ASYNC_is_capable() were first 320*e71b7053SJung-uk Kimadded to OpenSSL 1.1.0. 321*e71b7053SJung-uk Kim 322*e71b7053SJung-uk Kim=head1 COPYRIGHT 323*e71b7053SJung-uk Kim 324*e71b7053SJung-uk KimCopyright 2015-2016 The OpenSSL Project Authors. All Rights Reserved. 325*e71b7053SJung-uk Kim 326*e71b7053SJung-uk KimLicensed under the OpenSSL license (the "License"). You may not use 327*e71b7053SJung-uk Kimthis file except in compliance with the License. You can obtain a copy 328*e71b7053SJung-uk Kimin the file LICENSE in the source distribution or at 329*e71b7053SJung-uk KimL<https://www.openssl.org/source/license.html>. 330*e71b7053SJung-uk Kim 331*e71b7053SJung-uk Kim=cut 332