1 | /* |
---|
2 | * tclLoadAix.c -- |
---|
3 | * |
---|
4 | * This file implements the dlopen and dlsym APIs under the AIX operating |
---|
5 | * system, to enable the Tcl "load" command to work. This code was |
---|
6 | * provided by Jens-Uwe Mager. |
---|
7 | * |
---|
8 | * This file is subject to the following copyright notice, which is |
---|
9 | * different from the notice used elsewhere in Tcl. The file has been |
---|
10 | * modified to incorporate the file dlfcn.h in-line. |
---|
11 | * |
---|
12 | * Copyright (c) 1992,1993,1995,1996, Jens-Uwe Mager, Helios Software GmbH |
---|
13 | * Not derived from licensed software. |
---|
14 | * |
---|
15 | * Permission is granted to freely use, copy, modify, and redistribute |
---|
16 | * this software, provided that the author is not construed to be liable |
---|
17 | * for any results of using the software, alterations are clearly marked |
---|
18 | * as such, and this notice is not modified. |
---|
19 | * |
---|
20 | * RCS: @(#) $Id: tclLoadAix.c,v 1.6 2007/04/16 13:36:36 dkf Exp $ |
---|
21 | * |
---|
22 | * Note: this file has been altered from the original in a few ways in order |
---|
23 | * to work properly with Tcl. |
---|
24 | */ |
---|
25 | |
---|
26 | /* |
---|
27 | * @(#)dlfcn.c 1.7 revision of 95/08/14 19:08:38 |
---|
28 | * This is an unpublished work copyright (c) 1992 HELIOS Software GmbH |
---|
29 | * 30159 Hannover, Germany |
---|
30 | */ |
---|
31 | |
---|
32 | #include <stdio.h> |
---|
33 | #include <errno.h> |
---|
34 | #include <string.h> |
---|
35 | #include <stdlib.h> |
---|
36 | #include <sys/types.h> |
---|
37 | #include <sys/ldr.h> |
---|
38 | #include <a.out.h> |
---|
39 | #include <ldfcn.h> |
---|
40 | #include "../compat/dlfcn.h" |
---|
41 | |
---|
42 | /* |
---|
43 | * We simulate dlopen() et al. through a call to load. Because AIX has no call |
---|
44 | * to find an exported symbol we read the loader section of the loaded module |
---|
45 | * and build a list of exported symbols and their virtual address. |
---|
46 | */ |
---|
47 | |
---|
48 | typedef struct { |
---|
49 | char *name; /* The symbols's name. */ |
---|
50 | void *addr; /* Its relocated virtual address. */ |
---|
51 | } Export, *ExportPtr; |
---|
52 | |
---|
53 | /* |
---|
54 | * xlC uses the following structure to list its constructors and destructors. |
---|
55 | * This is gleaned from the output of munch. |
---|
56 | */ |
---|
57 | |
---|
58 | typedef struct { |
---|
59 | void (*init)(void); /* call static constructors */ |
---|
60 | void (*term)(void); /* call static destructors */ |
---|
61 | } Cdtor, *CdtorPtr; |
---|
62 | |
---|
63 | /* |
---|
64 | * The void * handle returned from dlopen is actually a ModulePtr. |
---|
65 | */ |
---|
66 | |
---|
67 | typedef struct Module { |
---|
68 | struct Module *next; |
---|
69 | char *name; /* module name for refcounting */ |
---|
70 | int refCnt; /* the number of references */ |
---|
71 | void *entry; /* entry point from load */ |
---|
72 | struct dl_info *info; /* optional init/terminate functions */ |
---|
73 | CdtorPtr cdtors; /* optional C++ constructors */ |
---|
74 | int nExports; /* the number of exports found */ |
---|
75 | ExportPtr exports; /* the array of exports */ |
---|
76 | } Module, *ModulePtr; |
---|
77 | |
---|
78 | /* |
---|
79 | * We keep a list of all loaded modules to be able to call the fini handlers |
---|
80 | * and destructors at atexit() time. |
---|
81 | */ |
---|
82 | |
---|
83 | static ModulePtr modList; |
---|
84 | |
---|
85 | /* |
---|
86 | * The last error from one of the dl* routines is kept in static variables |
---|
87 | * here. Each error is returned only once to the caller. |
---|
88 | */ |
---|
89 | |
---|
90 | static char errbuf[BUFSIZ]; |
---|
91 | static int errvalid; |
---|
92 | |
---|
93 | static void caterr(char *); |
---|
94 | static int readExports(ModulePtr); |
---|
95 | static void terminate(void); |
---|
96 | static void *findMain(void); |
---|
97 | |
---|
98 | void * |
---|
99 | dlopen( |
---|
100 | const char *path, |
---|
101 | int mode) |
---|
102 | { |
---|
103 | register ModulePtr mp; |
---|
104 | static void *mainModule; |
---|
105 | |
---|
106 | /* |
---|
107 | * Upon the first call register a terminate handler that will close all |
---|
108 | * libraries. Also get a reference to the main module for use with |
---|
109 | * loadbind. |
---|
110 | */ |
---|
111 | |
---|
112 | if (!mainModule) { |
---|
113 | mainModule = findMain(); |
---|
114 | if (mainModule == NULL) { |
---|
115 | return NULL; |
---|
116 | } |
---|
117 | atexit(terminate); |
---|
118 | } |
---|
119 | |
---|
120 | /* |
---|
121 | * Scan the list of modules if we have the module already loaded. |
---|
122 | */ |
---|
123 | |
---|
124 | for (mp = modList; mp; mp = mp->next) { |
---|
125 | if (strcmp(mp->name, path) == 0) { |
---|
126 | mp->refCnt++; |
---|
127 | return (void *) mp; |
---|
128 | } |
---|
129 | } |
---|
130 | |
---|
131 | mp = (ModulePtr) calloc(1, sizeof(*mp)); |
---|
132 | if (mp == NULL) { |
---|
133 | errvalid++; |
---|
134 | strcpy(errbuf, "calloc: "); |
---|
135 | strcat(errbuf, strerror(errno)); |
---|
136 | return NULL; |
---|
137 | } |
---|
138 | |
---|
139 | mp->name = malloc((unsigned) (strlen(path) + 1)); |
---|
140 | strcpy(mp->name, path); |
---|
141 | |
---|
142 | /* |
---|
143 | * load should be declared load(const char *...). Thus we cast the path to |
---|
144 | * a normal char *. Ugly. |
---|
145 | */ |
---|
146 | |
---|
147 | mp->entry = (void *) load((char *)path, L_NOAUTODEFER, NULL); |
---|
148 | if (mp->entry == NULL) { |
---|
149 | free(mp->name); |
---|
150 | free(mp); |
---|
151 | errvalid++; |
---|
152 | strcpy(errbuf, "dlopen: "); |
---|
153 | strcat(errbuf, path); |
---|
154 | strcat(errbuf, ": "); |
---|
155 | |
---|
156 | /* |
---|
157 | * If AIX says the file is not executable, the error can be further |
---|
158 | * described by querying the loader about the last error. |
---|
159 | */ |
---|
160 | |
---|
161 | if (errno == ENOEXEC) { |
---|
162 | char *tmp[BUFSIZ/sizeof(char *)], **p; |
---|
163 | |
---|
164 | if (loadquery(L_GETMESSAGES, tmp, sizeof(tmp)) == -1) { |
---|
165 | strcpy(errbuf, strerror(errno)); |
---|
166 | } else { |
---|
167 | for (p=tmp ; *p ; p++) { |
---|
168 | caterr(*p); |
---|
169 | } |
---|
170 | } |
---|
171 | } else { |
---|
172 | strcat(errbuf, strerror(errno)); |
---|
173 | } |
---|
174 | return NULL; |
---|
175 | } |
---|
176 | |
---|
177 | mp->refCnt = 1; |
---|
178 | mp->next = modList; |
---|
179 | modList = mp; |
---|
180 | |
---|
181 | if (loadbind(0, mainModule, mp->entry) == -1) { |
---|
182 | loadbindFailure: |
---|
183 | dlclose(mp); |
---|
184 | errvalid++; |
---|
185 | strcpy(errbuf, "loadbind: "); |
---|
186 | strcat(errbuf, strerror(errno)); |
---|
187 | return NULL; |
---|
188 | } |
---|
189 | |
---|
190 | /* |
---|
191 | * If the user wants global binding, loadbind against all other loaded |
---|
192 | * modules. |
---|
193 | */ |
---|
194 | |
---|
195 | if (mode & RTLD_GLOBAL) { |
---|
196 | register ModulePtr mp1; |
---|
197 | |
---|
198 | for (mp1 = mp->next; mp1; mp1 = mp1->next) { |
---|
199 | if (loadbind(0, mp1->entry, mp->entry) == -1) { |
---|
200 | goto loadbindFailure; |
---|
201 | } |
---|
202 | } |
---|
203 | } |
---|
204 | |
---|
205 | if (readExports(mp) == -1) { |
---|
206 | dlclose(mp); |
---|
207 | return NULL; |
---|
208 | } |
---|
209 | |
---|
210 | /* |
---|
211 | * If there is a dl_info structure, call the init function. |
---|
212 | */ |
---|
213 | |
---|
214 | if (mp->info = (struct dl_info *)dlsym(mp, "dl_info")) { |
---|
215 | if (mp->info->init) { |
---|
216 | (*mp->info->init)(); |
---|
217 | } |
---|
218 | } else { |
---|
219 | errvalid = 0; |
---|
220 | } |
---|
221 | |
---|
222 | /* |
---|
223 | * If the shared object was compiled using xlC we will need to call static |
---|
224 | * constructors (and later on dlclose destructors). |
---|
225 | */ |
---|
226 | |
---|
227 | if (mp->cdtors = (CdtorPtr) dlsym(mp, "__cdtors")) { |
---|
228 | while (mp->cdtors->init) { |
---|
229 | (*mp->cdtors->init)(); |
---|
230 | mp->cdtors++; |
---|
231 | } |
---|
232 | } else { |
---|
233 | errvalid = 0; |
---|
234 | } |
---|
235 | |
---|
236 | return (void *) mp; |
---|
237 | } |
---|
238 | |
---|
239 | /* |
---|
240 | * Attempt to decipher an AIX loader error message and append it to our static |
---|
241 | * error message buffer. |
---|
242 | */ |
---|
243 | |
---|
244 | static void |
---|
245 | caterr( |
---|
246 | char *s) |
---|
247 | { |
---|
248 | register char *p = s; |
---|
249 | |
---|
250 | while (*p >= '0' && *p <= '9') { |
---|
251 | p++; |
---|
252 | } |
---|
253 | switch (atoi(s)) { /* INTL: "C", UTF safe. */ |
---|
254 | case L_ERROR_TOOMANY: |
---|
255 | strcat(errbuf, "to many errors"); |
---|
256 | break; |
---|
257 | case L_ERROR_NOLIB: |
---|
258 | strcat(errbuf, "can't load library"); |
---|
259 | strcat(errbuf, p); |
---|
260 | break; |
---|
261 | case L_ERROR_UNDEF: |
---|
262 | strcat(errbuf, "can't find symbol"); |
---|
263 | strcat(errbuf, p); |
---|
264 | break; |
---|
265 | case L_ERROR_RLDBAD: |
---|
266 | strcat(errbuf, "bad RLD"); |
---|
267 | strcat(errbuf, p); |
---|
268 | break; |
---|
269 | case L_ERROR_FORMAT: |
---|
270 | strcat(errbuf, "bad exec format in"); |
---|
271 | strcat(errbuf, p); |
---|
272 | break; |
---|
273 | case L_ERROR_ERRNO: |
---|
274 | strcat(errbuf, strerror(atoi(++p))); /* INTL: "C", UTF safe. */ |
---|
275 | break; |
---|
276 | default: |
---|
277 | strcat(errbuf, s); |
---|
278 | break; |
---|
279 | } |
---|
280 | } |
---|
281 | |
---|
282 | void * |
---|
283 | dlsym( |
---|
284 | void *handle, |
---|
285 | const char *symbol) |
---|
286 | { |
---|
287 | register ModulePtr mp = (ModulePtr)handle; |
---|
288 | register ExportPtr ep; |
---|
289 | register int i; |
---|
290 | |
---|
291 | /* |
---|
292 | * Could speed up the search, but I assume that one assigns the result to |
---|
293 | * function pointers anyways. |
---|
294 | */ |
---|
295 | |
---|
296 | for (ep = mp->exports, i = mp->nExports; i; i--, ep++) { |
---|
297 | if (strcmp(ep->name, symbol) == 0) { |
---|
298 | return ep->addr; |
---|
299 | } |
---|
300 | } |
---|
301 | |
---|
302 | errvalid++; |
---|
303 | strcpy(errbuf, "dlsym: undefined symbol "); |
---|
304 | strcat(errbuf, symbol); |
---|
305 | return NULL; |
---|
306 | } |
---|
307 | |
---|
308 | char * |
---|
309 | dlerror(void) |
---|
310 | { |
---|
311 | if (errvalid) { |
---|
312 | errvalid = 0; |
---|
313 | return errbuf; |
---|
314 | } |
---|
315 | return NULL; |
---|
316 | } |
---|
317 | |
---|
318 | int |
---|
319 | dlclose( |
---|
320 | void *handle) |
---|
321 | { |
---|
322 | register ModulePtr mp = (ModulePtr)handle; |
---|
323 | int result; |
---|
324 | register ModulePtr mp1; |
---|
325 | |
---|
326 | if (--mp->refCnt > 0) { |
---|
327 | return 0; |
---|
328 | } |
---|
329 | |
---|
330 | if (mp->info && mp->info->fini) { |
---|
331 | (*mp->info->fini)(); |
---|
332 | } |
---|
333 | |
---|
334 | if (mp->cdtors) { |
---|
335 | while (mp->cdtors->term) { |
---|
336 | (*mp->cdtors->term)(); |
---|
337 | mp->cdtors++; |
---|
338 | } |
---|
339 | } |
---|
340 | |
---|
341 | result = unload(mp->entry); |
---|
342 | if (result == -1) { |
---|
343 | errvalid++; |
---|
344 | strcpy(errbuf, strerror(errno)); |
---|
345 | } |
---|
346 | |
---|
347 | if (mp->exports) { |
---|
348 | register ExportPtr ep; |
---|
349 | register int i; |
---|
350 | for (ep = mp->exports, i = mp->nExports; i; i--, ep++) { |
---|
351 | if (ep->name) { |
---|
352 | free(ep->name); |
---|
353 | } |
---|
354 | } |
---|
355 | free(mp->exports); |
---|
356 | } |
---|
357 | |
---|
358 | if (mp == modList) { |
---|
359 | modList = mp->next; |
---|
360 | } else { |
---|
361 | for (mp1 = modList; mp1; mp1 = mp1->next) { |
---|
362 | if (mp1->next == mp) { |
---|
363 | mp1->next = mp->next; |
---|
364 | break; |
---|
365 | } |
---|
366 | } |
---|
367 | } |
---|
368 | |
---|
369 | free(mp->name); |
---|
370 | free(mp); |
---|
371 | return result; |
---|
372 | } |
---|
373 | |
---|
374 | static void |
---|
375 | terminate(void) |
---|
376 | { |
---|
377 | while (modList) { |
---|
378 | dlclose(modList); |
---|
379 | } |
---|
380 | } |
---|
381 | |
---|
382 | /* |
---|
383 | * Build the export table from the XCOFF .loader section. |
---|
384 | */ |
---|
385 | |
---|
386 | static int |
---|
387 | readExports( |
---|
388 | ModulePtr mp) |
---|
389 | { |
---|
390 | LDFILE *ldp = NULL; |
---|
391 | SCNHDR sh, shdata; |
---|
392 | LDHDR *lhp; |
---|
393 | char *ldbuf; |
---|
394 | LDSYM *ls; |
---|
395 | int i; |
---|
396 | ExportPtr ep; |
---|
397 | const char *errMsg; |
---|
398 | |
---|
399 | #define Error(msg) do{errMsg=(msg);goto error;}while(0) |
---|
400 | #define SysErr() Error(strerror(errno)) |
---|
401 | |
---|
402 | ldp = ldopen(mp->name, ldp); |
---|
403 | if (ldp == NULL) { |
---|
404 | struct ld_info *lp; |
---|
405 | char *buf; |
---|
406 | int size = 0; |
---|
407 | |
---|
408 | if (errno != ENOENT) { |
---|
409 | SysErr(); |
---|
410 | } |
---|
411 | |
---|
412 | /* |
---|
413 | * The module might be loaded due to the LIBPATH environment variable. |
---|
414 | * Search for the loaded module using L_GETINFO. |
---|
415 | */ |
---|
416 | |
---|
417 | while (1) { |
---|
418 | size += 4 * 1024; |
---|
419 | buf = malloc(size); |
---|
420 | if (buf == NULL) { |
---|
421 | SysErr(); |
---|
422 | } |
---|
423 | |
---|
424 | i = loadquery(L_GETINFO, buf, size); |
---|
425 | |
---|
426 | if (i != -1) { |
---|
427 | break; |
---|
428 | } |
---|
429 | free(buf); |
---|
430 | if (errno != ENOMEM) { |
---|
431 | SysErr(); |
---|
432 | } |
---|
433 | } |
---|
434 | |
---|
435 | /* |
---|
436 | * Traverse the list of loaded modules. The entry point returned by |
---|
437 | * load() does actually point to the data segment origin. |
---|
438 | */ |
---|
439 | |
---|
440 | lp = (struct ld_info *) buf; |
---|
441 | while (lp) { |
---|
442 | if (lp->ldinfo_dataorg == mp->entry) { |
---|
443 | ldp = ldopen(lp->ldinfo_filename, ldp); |
---|
444 | break; |
---|
445 | } |
---|
446 | if (lp->ldinfo_next == 0) { |
---|
447 | lp = NULL; |
---|
448 | } else { |
---|
449 | lp = (struct ld_info *)((char *)lp + lp->ldinfo_next); |
---|
450 | } |
---|
451 | } |
---|
452 | |
---|
453 | free(buf); |
---|
454 | |
---|
455 | if (!ldp) { |
---|
456 | SysErr(); |
---|
457 | } |
---|
458 | } |
---|
459 | |
---|
460 | if (TYPE(ldp) != U802TOCMAGIC) { |
---|
461 | Error("bad magic"); |
---|
462 | } |
---|
463 | |
---|
464 | /* |
---|
465 | * Get the padding for the data section. This is needed for AIX 4.1 |
---|
466 | * compilers. This is used when building the final function pointer to the |
---|
467 | * exported symbol. |
---|
468 | */ |
---|
469 | |
---|
470 | if (ldnshread(ldp, _DATA, &shdata) != SUCCESS) { |
---|
471 | Error("cannot read data section header"); |
---|
472 | } |
---|
473 | |
---|
474 | if (ldnshread(ldp, _LOADER, &sh) != SUCCESS) { |
---|
475 | Error("cannot read loader section header"); |
---|
476 | } |
---|
477 | |
---|
478 | /* |
---|
479 | * We read the complete loader section in one chunk, this makes finding |
---|
480 | * long symbol names residing in the string table easier. |
---|
481 | */ |
---|
482 | |
---|
483 | ldbuf = (char *) malloc(sh.s_size); |
---|
484 | if (ldbuf == NULL) { |
---|
485 | SysErr(); |
---|
486 | } |
---|
487 | |
---|
488 | if (FSEEK(ldp, sh.s_scnptr, BEGINNING) != OKFSEEK) { |
---|
489 | free(ldbuf); |
---|
490 | Error("cannot seek to loader section"); |
---|
491 | } |
---|
492 | |
---|
493 | if (FREAD(ldbuf, sh.s_size, 1, ldp) != 1) { |
---|
494 | free(ldbuf); |
---|
495 | Error("cannot read loader section"); |
---|
496 | } |
---|
497 | |
---|
498 | lhp = (LDHDR *) ldbuf; |
---|
499 | ls = (LDSYM *)(ldbuf + LDHDRSZ); |
---|
500 | |
---|
501 | /* |
---|
502 | * Count the number of exports to include in our export table. |
---|
503 | */ |
---|
504 | |
---|
505 | for (i = lhp->l_nsyms; i; i--, ls++) { |
---|
506 | if (!LDR_EXPORT(*ls)) { |
---|
507 | continue; |
---|
508 | } |
---|
509 | mp->nExports++; |
---|
510 | } |
---|
511 | |
---|
512 | mp->exports = (ExportPtr) calloc(mp->nExports, sizeof(*mp->exports)); |
---|
513 | if (mp->exports == NULL) { |
---|
514 | free(ldbuf); |
---|
515 | SysErr(); |
---|
516 | } |
---|
517 | |
---|
518 | /* |
---|
519 | * Fill in the export table. All entries are relative to the entry point |
---|
520 | * we got from load. |
---|
521 | */ |
---|
522 | |
---|
523 | ep = mp->exports; |
---|
524 | ls = (LDSYM *)(ldbuf + LDHDRSZ); |
---|
525 | for (i=lhp->l_nsyms ; i!=0 ; i--,ls++) { |
---|
526 | char *symname; |
---|
527 | char tmpsym[SYMNMLEN+1]; |
---|
528 | |
---|
529 | if (!LDR_EXPORT(*ls)) { |
---|
530 | continue; |
---|
531 | } |
---|
532 | |
---|
533 | if (ls->l_zeroes == 0) { |
---|
534 | symname = ls->l_offset + lhp->l_stoff + ldbuf; |
---|
535 | } else { |
---|
536 | /* |
---|
537 | * The l_name member is not zero terminated, we must copy the |
---|
538 | * first SYMNMLEN chars and make sure we have a zero byte at the |
---|
539 | * end. |
---|
540 | */ |
---|
541 | |
---|
542 | strncpy(tmpsym, ls->l_name, SYMNMLEN); |
---|
543 | tmpsym[SYMNMLEN] = '\0'; |
---|
544 | symname = tmpsym; |
---|
545 | } |
---|
546 | ep->name = malloc((unsigned) (strlen(symname) + 1)); |
---|
547 | strcpy(ep->name, symname); |
---|
548 | ep->addr = (void *)((unsigned long) |
---|
549 | mp->entry + ls->l_value - shdata.s_vaddr); |
---|
550 | ep++; |
---|
551 | } |
---|
552 | free(ldbuf); |
---|
553 | while (ldclose(ldp) == FAILURE) { |
---|
554 | /* Empty body */ |
---|
555 | } |
---|
556 | return 0; |
---|
557 | |
---|
558 | /* |
---|
559 | * This is a factoring out of the error-handling code to make the rest of |
---|
560 | * the function much simpler to read. |
---|
561 | */ |
---|
562 | |
---|
563 | error: |
---|
564 | errvalid++; |
---|
565 | strcpy(errbuf, "readExports: "); |
---|
566 | strcat(errbuf, errMsg); |
---|
567 | |
---|
568 | if (ldp != NULL) { |
---|
569 | while (ldclose(ldp) == FAILURE) { |
---|
570 | /* Empty body */ |
---|
571 | } |
---|
572 | } |
---|
573 | return -1; |
---|
574 | } |
---|
575 | |
---|
576 | /* |
---|
577 | * Find the main modules entry point. This is used as export pointer for |
---|
578 | * loadbind() to be able to resolve references to the main part. |
---|
579 | */ |
---|
580 | |
---|
581 | static void * |
---|
582 | findMain(void) |
---|
583 | { |
---|
584 | struct ld_info *lp; |
---|
585 | char *buf; |
---|
586 | int size = 4*1024; |
---|
587 | int i; |
---|
588 | void *ret; |
---|
589 | |
---|
590 | buf = malloc(size); |
---|
591 | if (buf == NULL) { |
---|
592 | goto error; |
---|
593 | } |
---|
594 | |
---|
595 | while ((i = loadquery(L_GETINFO, buf, size)) == -1 && errno == ENOMEM) { |
---|
596 | free(buf); |
---|
597 | size += 4*1024; |
---|
598 | buf = malloc(size); |
---|
599 | if (buf == NULL) { |
---|
600 | goto error; |
---|
601 | } |
---|
602 | } |
---|
603 | |
---|
604 | if (i == -1) { |
---|
605 | free(buf); |
---|
606 | goto error; |
---|
607 | } |
---|
608 | |
---|
609 | /* |
---|
610 | * The first entry is the main module. The entry point returned by load() |
---|
611 | * does actually point to the data segment origin. |
---|
612 | */ |
---|
613 | |
---|
614 | lp = (struct ld_info *) buf; |
---|
615 | ret = lp->ldinfo_dataorg; |
---|
616 | free(buf); |
---|
617 | return ret; |
---|
618 | |
---|
619 | error: |
---|
620 | errvalid++; |
---|
621 | strcpy(errbuf, "findMain: "); |
---|
622 | strcat(errbuf, strerror(errno)); |
---|
623 | return NULL; |
---|
624 | } |
---|
625 | |
---|
626 | /* |
---|
627 | * Local Variables: |
---|
628 | * mode: c |
---|
629 | * c-basic-offset: 4 |
---|
630 | * fill-column: 78 |
---|
631 | * End: |
---|
632 | */ |
---|