source: roaraudio/libroar/scheduler.c @ 5694:d31d31565f84

Last change on this file since 5694:d31d31565f84 was 5694:d31d31565f84, checked in by phi, 12 years ago

small updates to scheduler API

File size: 18.2 KB
Line 
1//scheduler.c:
2
3/*
4 *      Copyright (C) Philipp 'ph3-der-loewe' Schafft - 2012
5 *
6 *  This file is part of libroar a part of RoarAudio,
7 *  a cross-platform sound system for both, home and professional use.
8 *  See README for details.
9 *
10 *  This file is free software; you can redistribute it and/or modify
11 *  it under the terms of the GNU General Public License version 3
12 *  as published by the Free Software Foundation.
13 *
14 *  libroar is distributed in the hope that it will be useful,
15 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
16 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 *  GNU General Public License for more details.
18 *
19 *  You should have received a copy of the GNU General Public License
20 *  along with this software; see the file COPYING.  If not, write to
21 *  the Free Software Foundation, 51 Franklin Street, Fifth Floor,
22 *  Boston, MA 02110-1301, USA.
23 *
24 *  NOTE for everyone want's to change something and send patches:
25 *  read README and HACKING! There a addition information on
26 *  the license of this document you need to read before you send
27 *  any patches.
28 *
29 *  NOTE for uses of non-GPL (LGPL,...) software using libesd, libartsc
30 *  or libpulse*:
31 *  The libs libroaresd, libroararts and libroarpulse link this lib
32 *  and are therefore GPL. Because of this it may be illigal to use
33 *  them with any software that uses libesd, libartsc or libpulse*.
34 */
35
36#include "libroar.h"
37
38#define INIT_SIZE   8
39#define MAX_PROTOS 16
40
41struct protocol {
42 const struct roar_keyval * para;
43 ssize_t paralen;
44 struct roar_dl_lhandle * lhandle;
45 const struct roar_dl_proto * impl;
46};
47
48struct roar_scheduler {
49 size_t refc;
50 int flags;
51 enum roar_scheduler_strategy strategy;
52 struct roar_scheduler_source ** sources;
53 size_t sources_len;
54 struct roar_vio_select * vios;
55 size_t vios_len;
56 struct roar_dl_fnreg callback;
57 struct protocol protos[MAX_PROTOS];
58};
59
60struct roar_scheduler * roar_scheduler_new(int flags, enum roar_scheduler_strategy strategy) {
61 struct roar_scheduler * sched = roar_mm_malloc(sizeof(struct roar_scheduler));
62
63 if ( sched == NULL )
64  return NULL;
65
66 if ( flags == ROAR_SCHEDULER_FLAG_DEFAULT )
67  flags = ROAR_SCHEDULER_FLAG_NONE;
68
69 memset(sched, 0, sizeof(struct roar_scheduler));
70 sched->refc = 1;
71 sched->flags = flags;
72 sched->strategy = strategy;
73
74 sched->sources = roar_mm_malloc(INIT_SIZE*sizeof(struct roar_scheduler_source *));
75 if ( sched->sources != NULL ) {
76  sched->sources_len = INIT_SIZE;
77  memset(sched->sources, 0, INIT_SIZE*sizeof(struct roar_scheduler_source *));
78 }
79
80 sched->vios = roar_mm_malloc(sched->sources_len*sizeof(struct roar_vio_select));
81 if ( sched->vios != 0 )
82  sched->vios_len = sched->sources_len;
83
84 return sched;
85}
86
87#define _CHKSCHED(extra) if ( sched == NULL || (extra) ) { roar_err_set(ROAR_ERROR_FAULT); return -1; }
88
89int                     roar_scheduler_ref(struct roar_scheduler * sched) {
90 _CHKSCHED(0);
91
92 sched->refc++;
93
94 return 0;
95}
96
97int                     roar_scheduler_unref(struct roar_scheduler * sched) {
98 size_t i;
99
100 _CHKSCHED(0);
101
102 sched->refc--;
103
104 if (sched->refc)
105  return 0;
106
107 for (i = 0; i < sched->sources_len; i++)
108  if ( sched->sources[i] != NULL )
109   roar_scheduler_source_del(sched, sched->sources[i]);
110
111 if ( sched->vios != NULL )
112  roar_mm_free(sched->vios);
113 if ( sched->sources != NULL )
114  roar_mm_free(sched->sources);
115 roar_mm_free(sched);
116 return 0;
117}
118
119static void __delete_cpi_client(struct roar_scheduler * sched, struct roar_scheduler_source * cur, struct roar_dl_librarypara * para) {
120 if ( cur->handle.cpi.impl->unset_proto != NULL )
121  cur->handle.cpi.impl->unset_proto(cur->handle.cpi.client, cur->vio, &(cur->handle.cpi.obuffer), &(cur->handle.cpi.userdata), cur->handle.cpi.protopara, cur->handle.cpi.protoparalen, para);
122
123 roar_vio_close(cur->vio);
124 cur->vio = NULL;
125
126 if ( cur->handle.cpi.obuffer != NULL ) {
127  roar_buffer_free(cur->handle.cpi.obuffer);
128  cur->handle.cpi.obuffer = NULL;
129 }
130
131 if ( cur->handle.cpi.userdata != NULL ) {
132  roar_mm_free(cur->handle.cpi.userdata);
133  cur->handle.cpi.userdata = NULL;
134 }
135
136 roar_scheduler_source_del(sched, cur);
137}
138
139static int __flush_cpi_client(struct roar_scheduler * sched, struct roar_scheduler_source * cur, struct roar_dl_librarypara * para) {
140 size_t len;
141 ssize_t ret;
142 void * buf;
143
144 if ( roar_buffer_get_len(cur->handle.cpi.obuffer, &len) == -1 )
145  return 0;
146
147 if ( roar_buffer_get_data(cur->handle.cpi.obuffer, &buf) == -1 )
148  return 0;
149
150 ret = roar_vio_write(cur->vio, buf, len);
151
152 if ( ret < 1 )
153  return -1;
154
155 if ( ret == (ssize_t)len ) {
156  if ( roar_buffer_next(&(cur->handle.cpi.obuffer)) == -1 )
157   return -1;
158 } else {
159  if ( roar_buffer_set_offset(cur->handle.cpi.obuffer, ret) == -1 )
160   return -1;
161 }
162
163 return 0;
164}
165
166// what to do?:
167// 0. get all VIOs.
168// 1. get timeout or use internal default.
169// 2. run roar_vio_select().
170// 3. send all events based on results.
171// 4. send UPDATE to all plugins and containers.
172int                     roar_scheduler_iterate(struct roar_scheduler * sched) {
173 struct roar_vio_selecttv timeout = {8, 0}; // default: 8 sec. Just a random value.
174 struct roar_scheduler_source * cur, * new_client;
175 struct roar_dl_librarypara * para;
176 struct roar_dl_lhandle * lhandle;
177 ssize_t ret;
178 size_t i;
179 int have_timeout = 0;
180 size_t todo = 0;
181 int tmp;
182
183 _CHKSCHED(0);
184
185 if ( sched->vios == NULL || sched->vios_len < sched->sources_len ) {
186  if ( sched->vios != NULL ) {
187   roar_mm_free(sched->vios);
188   sched->vios = NULL;
189   sched->vios_len = 0;
190  }
191
192  sched->vios = roar_mm_malloc(sched->sources_len*sizeof(struct roar_vio_select));
193  if ( sched->vios != NULL )
194   sched->vios_len = sched->sources_len;
195 }
196
197 // error from roar_mm_malloc() is still set.
198 if ( sched->vios == NULL )
199  return -1;
200
201 memset(sched->vios, 0, sched->vios_len*sizeof(struct roar_vio_select));
202 for (i = 0; i < sched->vios_len; i++)
203  sched->vios[i].eventsq = ROAR_VIO_SELECT_NO_RETEST;
204
205 for (i = 0; i < sched->sources_len; i++) {
206  if ( (cur = sched->sources[i]) == NULL )
207   continue;
208  switch (cur->type) {
209   case ROAR_SCHEDULER_VIO:
210     ROAR_VIO_SELECT_SETVIO(&(sched->vios[i]), cur->vio, cur->handle.eventsq);
211     todo++;
212    break;
213   case ROAR_SCHEDULER_TIMEOUT:
214     timeout = cur->handle.timeout;
215     have_timeout = 1;
216     todo++;
217    break;
218   case ROAR_SCHEDULER_CPI_LISTEN:
219     ROAR_VIO_SELECT_SETVIO(&(sched->vios[i]), cur->vio, ROAR_VIO_SELECT_READ);
220     todo++;
221    break;
222   case ROAR_SCHEDULER_CPI_CLIENT:
223     tmp = 0;
224
225     if ( cur->handle.cpi.impl->status != NULL ) {
226      if ( cur->lhandle != NULL )
227       roar_dl_context_restore(cur->lhandle);
228
229      para = roar_dl_getpara(cur->lhandle);
230      if ( cur->handle.cpi.impl->status(cur->handle.cpi.client, cur->vio, &(cur->handle.cpi.obuffer), &(cur->handle.cpi.userdata), cur->handle.cpi.protopara, cur->handle.cpi.protoparalen, para) & ROAR_DL_PROTO_STATUS_RX_READY )
231       tmp |= ROAR_VIO_SELECT_READ;
232      if ( para != NULL )
233       roar_dl_para_unref(para);
234
235      if ( cur->lhandle != NULL )
236       roar_dl_context_store(cur->lhandle);
237     } else {
238      tmp |= ROAR_VIO_SELECT_READ;
239     }
240
241     if ( sched->sources[i]->handle.cpi.obuffer != NULL )
242      tmp |= ROAR_VIO_SELECT_WRITE;
243
244     ROAR_VIO_SELECT_SETVIO(&(sched->vios[i]), sched->sources[i]->vio, tmp);
245     todo++;
246    break;
247   case ROAR_SCHEDULER_PLUGIN:
248   case ROAR_SCHEDULER_PLUGINCONTAINER:
249     todo++;
250    break;
251#ifndef DEBUG
252   default: /* noop */ break;
253#endif
254  }
255 }
256
257 if (!todo) {
258  roar_err_set(ROAR_ERROR_NOENT);
259  return 0;
260 }
261
262 ret = roar_vio_select(sched->vios, sched->vios_len, &timeout, NULL);
263 if ( ret == -1 )
264  return -1;
265 if ( ret == 0 && !have_timeout )
266  return 1;
267
268 for (i = 0; i < sched->sources_len; i++) {
269  if ( (cur = sched->sources[i]) == NULL )
270   continue;
271  switch (cur->type) {
272   case ROAR_SCHEDULER_VIO:
273     if ( sched->vios[i].eventsa )
274      if ( cur->cb != NULL )
275       cur->cb(sched->sources[i], sched->sources[i]->userdata, sched->vios[i].eventsa);
276    break;
277   case ROAR_SCHEDULER_TIMEOUT:
278     if ( ret == 0 )
279      if ( cur->cb != NULL )
280       cur->cb(sched->sources[i], sched->sources[i]->userdata, 0);
281    break;
282   case ROAR_SCHEDULER_PLUGIN:
283     roar_dl_appsched_trigger(cur->lhandle, ROAR_DL_APPSCHED_UPDATE);
284    break;
285   case ROAR_SCHEDULER_PLUGINCONTAINER:
286     roar_plugincontainer_appsched_trigger(cur->handle.container, ROAR_DL_APPSCHED_UPDATE);
287    break;
288   case ROAR_SCHEDULER_CPI_LISTEN:
289     if ( !sched->vios[i].eventsa )
290      continue;
291
292     if ( cur->cb != NULL )
293      cur->cb(sched->sources[i], sched->sources[i]->userdata, sched->vios[i].eventsa);
294
295     new_client = roar_mm_malloc(sizeof(struct roar_scheduler_source));
296     if ( new_client == NULL )
297      continue;
298     memcpy(new_client, cur, sizeof(struct roar_scheduler_source));
299     new_client->type = ROAR_SCHEDULER_CPI_CLIENT;
300     new_client->flags = ROAR_SCHEDULER_FLAG_FREE;
301     new_client->vio   = roar_mm_malloc(sizeof(struct roar_vio_calls));
302
303     if ( new_client->vio == NULL ) {
304      roar_mm_free(new_client);
305      continue;
306     }
307
308     if ( roar_vio_accept(new_client->vio, cur->vio) == -1 ) {
309      roar_mm_free(new_client->vio);
310      roar_mm_free(new_client);
311      continue;
312     }
313
314     new_client->vio->flags |= ROAR_VIO_FLAGS_FREESELF;
315
316     if ( roar_scheduler_source_add(sched, new_client) == -1 ) {
317      roar_vio_close(new_client->vio);
318      roar_mm_free(new_client);
319     }
320     roar_vio_unref(new_client->vio);
321
322     if ( new_client->cb != NULL )
323      new_client->cb(new_client, new_client->userdata, 0);
324
325     if ( cur->handle.cpi.impl->set_proto != NULL ) {
326      lhandle = new_client->lhandle;
327      para = roar_dl_getpara(lhandle);
328
329      if ( lhandle != NULL )
330       roar_dl_context_restore(lhandle);
331      new_client->handle.cpi.impl->set_proto(new_client->handle.cpi.client, new_client->vio, &(new_client->handle.cpi.obuffer), &(new_client->handle.cpi.userdata), new_client->handle.cpi.protopara, new_client->handle.cpi.protoparalen, para);
332      if ( lhandle != NULL )
333       roar_dl_context_store(lhandle);
334
335      if ( para != NULL )
336       roar_dl_para_unref(para);
337     }
338    break;
339   case ROAR_SCHEDULER_CPI_CLIENT:
340     if ( !sched->vios[i].eventsa )
341      continue;
342
343     if ( cur->cb != NULL )
344      cur->cb(sched->sources[i], sched->sources[i]->userdata, sched->vios[i].eventsa);
345
346     lhandle = cur->lhandle;
347     para = roar_dl_getpara(lhandle);
348     tmp = 0;
349
350     if ( sched->vios[i].eventsa & ROAR_VIO_SELECT_WRITE ) {
351      if ( cur->handle.cpi.impl->flush != NULL ) {
352       if ( lhandle != NULL )
353        roar_dl_context_restore(lhandle);
354
355       cur->handle.cpi.impl->flush(cur->handle.cpi.client, cur->vio, &(cur->handle.cpi.obuffer), &(cur->handle.cpi.userdata), cur->handle.cpi.protopara, cur->handle.cpi.protoparalen, para);
356
357       if ( lhandle != NULL )
358        roar_dl_context_store(lhandle);
359      } else {
360        tmp = __flush_cpi_client(sched, cur, para);
361      }
362
363      if ( tmp == 0 && cur->handle.cpi.obuffer == NULL && cur->handle.cpi.impl->flushed != NULL ) {
364       if ( lhandle != NULL )
365        roar_dl_context_restore(lhandle);
366
367       cur->handle.cpi.impl->flushed(cur->handle.cpi.client, cur->vio, &(cur->handle.cpi.obuffer), &(cur->handle.cpi.userdata), cur->handle.cpi.protopara, cur->handle.cpi.protoparalen, para);
368
369       if ( lhandle != NULL )
370        roar_dl_context_store(lhandle);
371      }
372     }
373
374     if ( sched->vios[i].eventsa & ROAR_VIO_SELECT_READ ) {
375      if ( cur->handle.cpi.impl->handle != NULL ) {
376       if ( lhandle != NULL )
377        roar_dl_context_restore(lhandle);
378
379       if ( cur->handle.cpi.impl->handle(cur->handle.cpi.client, cur->vio, &(cur->handle.cpi.obuffer), &(cur->handle.cpi.userdata), cur->handle.cpi.protopara, cur->handle.cpi.protoparalen, para) == -1 ) {
380        tmp = -1;
381       }
382
383       if ( lhandle != NULL )
384        roar_dl_context_store(lhandle);
385      }
386     }
387
388     if ( tmp == -1 ) {
389      if ( lhandle != NULL )
390       roar_dl_context_restore(lhandle);
391      __delete_cpi_client(sched, cur, para);
392      if ( lhandle != NULL )
393       roar_dl_context_store(lhandle);
394     }
395
396     if ( para != NULL )
397      roar_dl_para_unref(para);
398    break;
399#ifndef DEBUG
400   default: /* noop */ break;
401#endif
402  }
403 }
404
405 return 1;
406}
407
408int                     roar_scheduler_run(struct roar_scheduler * sched) {
409 int ret;
410
411 _CHKSCHED(0);
412
413 while ((ret = roar_scheduler_iterate(sched)) > 0);
414
415 return ret;
416}
417
418static int __cpi_callback(enum roar_dl_fnreg_action action, int fn, int subtype, const void * object, size_t objectlen, int version, int options, void * userdata, struct roar_dl_lhandle * lhandle) {
419 const struct roar_dl_proto * impl = object;
420 struct roar_scheduler * sched = userdata;
421 struct roar_dl_librarypara * para;
422 size_t i;
423
424 (void)fn, (void)subtype, (void)version, (void)options;
425
426 if ( sched == NULL ) {
427  roar_err_set(ROAR_ERROR_FAULT);
428  return -1;
429 }
430
431 if ( objectlen != ROAR_DL_PROTO_SIZE ) {
432  ROAR_WARN("__cpi_callback(*): Library %p tries to register protocol with bad object length.", lhandle);
433  roar_err_set(ROAR_ERROR_BADLIB);
434  return -1;
435 }
436
437 switch (action)  {
438  case ROAR_DL_FNREG:
439    for (i = 0; i < MAX_PROTOS; i++) {
440     if ( sched->protos[i].impl == NULL ) {
441      memset(&(sched->protos[i]), 0, sizeof(sched->protos[i]));
442      sched->protos[i].para = NULL;
443      sched->protos[i].paralen = -1;
444      sched->protos[i].lhandle = lhandle;
445      sched->protos[i].impl = impl;
446      return 0;
447     }
448    }
449   break;
450  case ROAR_DL_FNUNREG:
451    for (i = 0; i < sched->sources_len; i++) {
452     if ( sched->sources[i] == NULL )
453      continue;
454     if ( sched->sources[i]->type != ROAR_SCHEDULER_CPI_LISTEN && sched->sources[i]->type != ROAR_SCHEDULER_CPI_CLIENT )
455      continue;
456     if ( sched->sources[i]->handle.cpi.proto != impl->proto )
457      continue;
458
459     para = roar_dl_getpara(lhandle);
460
461     if ( lhandle != NULL )
462      roar_dl_context_restore(lhandle);
463     __delete_cpi_client(sched, sched->sources[i], para);
464     if ( lhandle != NULL )
465      roar_dl_context_store(lhandle);
466
467     if ( para != NULL )
468      roar_dl_para_unref(para);
469    }
470
471    for (i = 0; i < MAX_PROTOS; i++) {
472     if ( sched->protos[i].impl != NULL && sched->protos[i].lhandle == lhandle ) {
473      memset(&(sched->protos[i]), 0, sizeof(sched->protos[i]));
474      sched->protos[i].impl = NULL;
475     }
476    }
477    return 0;
478   break;
479 }
480
481 roar_err_set(ROAR_ERROR_NOSPC);
482 return -1;
483}
484
485static int __update_cpi_service (struct roar_scheduler * sched, struct roar_scheduler_source * source, int del) {
486 if ( del ) {
487  roar_err_set(ROAR_ERROR_NOTSUP);
488  return -1;
489 }
490
491 sched->callback.fn = ROAR_DL_FN_PROTO;
492 sched->callback.subtype = ROAR_DL_PROTO_SUBTYPE;
493 sched->callback.version = ROAR_DL_PROTO_VERSION;
494 sched->callback.callback = __cpi_callback;
495 sched->callback.userdata = sched;
496
497 ROAR_DL_RFNREG(ROAR_DL_HANDLE_LIBROAR, sched->callback);
498
499 return -1;
500}
501
502static int __update_cpi_listen_client (struct roar_scheduler * sched, struct roar_scheduler_source * source) {
503 size_t i;
504
505 ROAR_DBG("__update_cpi_listen_client(sched=%p, source=%p): proto=%i, impl=%p", sched, source, source->handle.cpi.proto, source->handle.cpi.impl);
506
507 if ( source->handle.cpi.proto < 1 && source->handle.cpi.impl != NULL )
508  source->handle.cpi.proto = source->handle.cpi.impl->proto;
509
510 if ( source->handle.cpi.proto > 0 && source->handle.cpi.impl == NULL ) {
511  for (i = 0; i < MAX_PROTOS; i++) {
512   if ( sched->protos[i].impl == NULL )
513    continue;
514   ROAR_DBG("__update_cpi_listen_client(sched=%p, source=%p): proto=%i<->%i", sched, source, sched->protos[i].impl->proto, source->handle.cpi.proto);
515   if ( sched->protos[i].impl->proto != source->handle.cpi.proto )
516    continue;
517   source->handle.cpi.impl = sched->protos[i].impl;
518   break;
519  }
520 }
521
522 if ( source->handle.cpi.proto > 0 && source->handle.cpi.impl != NULL )
523  return 0;
524
525 roar_err_set(ROAR_ERROR_INVAL);
526 return -1;
527}
528
529int                     roar_scheduler_source_add(struct roar_scheduler * sched,
530                                                  struct roar_scheduler_source * source) {
531 size_t i;
532 struct roar_scheduler_source ** next = NULL;
533 int err;
534 
535 _CHKSCHED(source == NULL);
536
537 ROAR_DBG("roar_scheduler_source_add(sched=%p, source=%p): proto=%i, impl=%p", sched, source, source->handle.cpi.proto, source->handle.cpi.impl);
538
539 for (i = 0; i < sched->sources_len; i++) {
540  if ( sched->sources[i] != NULL )
541   continue;
542  next = &(sched->sources[i]);
543  break;
544 }
545
546 if ( next == NULL ) {
547  // TODO: re-allocate some space here.
548  roar_err_set(ROAR_ERROR_NOSPC);
549  return -1;
550 }
551
552 if ( source->flags == ROAR_SCHEDULER_FLAG_DEFAULT )
553  source->flags = ROAR_SCHEDULER_FLAG_NONE;
554
555 switch (source->type) {
556  case ROAR_SCHEDULER_CPI_LISTEN:
557  case ROAR_SCHEDULER_CPI_CLIENT:
558    if ( __update_cpi_listen_client(sched, source) == -1 )
559     return -1;
560   break;
561  case ROAR_SCHEDULER_CPI_SERVICE:
562    if ( __update_cpi_service(sched, source, 0) == -1 )
563     return -1;
564   break;
565#ifndef DEBUG
566   default: /* noop */ break;
567#endif
568 }
569
570 if ( source->lhandle != NULL )
571  if ( roar_dl_ref(source->lhandle) == -1 )
572   return -1;
573
574 if ( source->vio != NULL ) {
575  if ( roar_vio_ref(source->vio) == -1 ) {
576   err = roar_error;
577   if ( source->lhandle != NULL )
578    roar_dl_unref(source->lhandle);
579   roar_err_set(err);
580   return -1;
581  }
582 }
583
584 if ( source->type == ROAR_SCHEDULER_PLUGINCONTAINER ) {
585  if ( roar_plugincontainer_ref(source->handle.container) == -1 ) {
586   err = roar_error;
587   if ( source->lhandle != NULL )
588    roar_dl_unref(source->lhandle);
589   if ( source->vio != NULL )
590    roar_vio_unref(source->vio);
591   roar_err_set(err);
592   return -1;
593  }
594 }
595
596 *next = source;
597
598 return 0;
599}
600
601int                     roar_scheduler_source_del(struct roar_scheduler * sched,
602                                                  struct roar_scheduler_source * source) {
603 size_t i;
604 struct roar_scheduler_source ** next = NULL;
605
606 _CHKSCHED(source == NULL);
607
608 for (i = 0; i < sched->sources_len; i++) {
609  if ( sched->sources[i] != source )
610   continue;
611  next = &(sched->sources[i]);
612  break;
613 }
614
615 if ( next == NULL ) {
616  roar_err_set(ROAR_ERROR_NOENT);
617  return -1;
618 }
619
620 switch (source->type) {
621  case ROAR_SCHEDULER_PLUGINCONTAINER:
622    roar_plugincontainer_ref(source->handle.container);
623   break;
624  case ROAR_SCHEDULER_CPI_SERVICE:
625    if ( __update_cpi_service(sched, source, 1) == -1 )
626     return -1;
627   break;
628#ifndef DEBUG
629   default: /* noop */ break;
630#endif
631 }
632
633 if ( source->lhandle != NULL )
634  roar_dl_unref(source->lhandle);
635 if ( source->vio != NULL )
636  roar_vio_unref(source->vio);
637
638 if ( source->flags & ROAR_SCHEDULER_FLAG_FREE )
639  roar_mm_free(source);
640
641 *next = NULL;
642 return 0;
643}
644
645//ll
Note: See TracBrowser for help on using the repository browser.