File Coverage

blib/lib/Const/Introspect/C.pm
Criterion Covered Total %
statement 134 137 97.8
branch 26 30 86.6
condition 8 14 57.1
subroutine 23 23 100.0
pod 4 4 100.0
total 195 208 93.7


line stmt bran cond sub pod time code
1             package Const::Introspect::C;
2              
3 2     2   242374 use Moo;
  2         12382  
  2         13  
4 2     2   1908 use 5.020;
  2         9  
5 2     2   648 use experimental qw( signatures postderef );
  2         3593  
  2         14  
6 2     2   1510 use Ref::Util qw( is_plain_arrayref );
  2         3375  
  2         131  
7 2     2   15 use Config;
  2         3  
  2         100  
8 2     2   1051 use Text::ParseWords ();
  2         2864  
  2         57  
9 2     2   1864 use Path::Tiny ();
  2         23531  
  2         67  
10 2     2   1282 use Capture::Tiny qw( capture capture_merged );
  2         53441  
  2         150  
11 2     2   511 use Const::Introspect::C::Constant;
  2         6  
  2         77  
12 2     2   1025 use Data::Section::Simple ();
  2         1274  
  2         50  
13 2     2   1070 use Template ();
  2         40623  
  2         65  
14 2     2   1651 use FFI::Platypus 1.00;
  2         14896  
  2         90  
15 2     2   1073 use FFI::Build;
  2         21518  
  2         4034  
16              
17             # ABSTRACT: Find and evaluate C/C++ constants for use in Perl
18             our $VERSION = '0.01'; # VERSION
19              
20              
21             has headers => (
22             is => 'ro',
23             isa => sub { die "headers should be a plain array ref" unless is_plain_arrayref($_[0]) },
24             default => sub { [] },
25             );
26              
27              
28             has lang => (
29             is => 'ro',
30             isa => sub {
31             die "lang should be one of c or c++" unless $_[0] =~ /^c(|\+\+)$/;
32             },
33             default => 'c',
34             );
35              
36              
37             has cc => (
38             is => 'ro',
39             default => sub { [Text::ParseWords::shellwords($Config{cc})] },
40             );
41              
42              
43             has ppflags => (
44             is => 'ro',
45             lazy => 1,
46             default => sub ($self) {
47             ['-dM', '-E', '-x' => $self->lang];
48             },
49             );
50              
51              
52             has cflags => (
53             is => 'ro',
54             default => sub { [Text::ParseWords::shellwords($Config{ccflags})] },
55             );
56              
57              
58             has extra_cflags => (
59             is => 'ro',
60             default => sub { [] },
61             );
62              
63              
64             has source => (
65             is => 'ro',
66             lazy => 1,
67             default => sub ($self) {
68             my $p = Path::Tiny->tempfile(
69             TEMPLATE => 'const-introspect-c-XXXXXX',
70             SUFFIX => $self->lang eq 'c' ? '.c' : '.cxx',
71             );
72             my $fh = $p->openw_utf8;
73             say $fh "#include <$_>" for $self->headers->@*;
74             close $fh;
75             $p;
76             },
77             );
78              
79              
80             has filter => (
81             is => 'ro',
82             default => sub {
83             qr/^[^_]/;
84             },
85             );
86              
87              
88             has diag => (
89             is => 'ro',
90             default => sub { [] },
91             );
92              
93              
94             sub get_macro_constants ($self)
95 1     1 1 674 {
  1         3  
  1         2  
96             my @cmd = (
97 1         31 map { $_->@* }
  4         12  
98             $self->cc,
99             $self->ppflags,
100             $self->cflags,
101             $self->extra_cflags,
102             );
103              
104 1         23 push @cmd, $self->source->stringify;
105              
106             my($out, $err, $ret, $sig) = capture {
107 1     1   18649 system @cmd;
108 1         89 ($? >> 8,$? & 127);
109 1         66 };
110              
111 1 50 33     1489 if($ret != 0 || $sig != 0)
112             {
113 0         0 push $self->diag->@*, $err;
114             # TODO: class exception here
115 0         0 die "command: @cmd failed";
116             }
117              
118 1         22 my $filter = $self->filter;
119              
120 1         7 my @macros;
121              
122 1         89 foreach my $line (split /\n/, $out)
123             {
124 256 50       837 if($line =~ /^#define\s+(\S+)\s+(.*)\s*$/)
125             {
126 256         458 my $name = $1;
127 256         376 my $value = $2;
128 256 100       509 next if $name =~ /[()]/;
129 243 100       761 next unless $name =~ $filter;
130              
131 7 100       65 if($value =~ /^-?([1-9][0-9]*|0[0-7]*)$/)
    100          
    100          
132             {
133 3         89 push @macros, Const::Introspect::C::Constant->new(
134             c => $self,
135             name => $name,
136             raw_value => $value,
137             value => int $value,
138             type => 'int',
139             )
140             }
141             elsif($value =~ /^"([a-z_0-9]+)"$/i)
142             {
143 1         33 push @macros, Const::Introspect::C::Constant->new(
144             c => $self,
145             name => $name,
146             raw_value => $value,
147             value => $1,
148             type => 'string',
149             )
150             }
151             elsif($value =~ /^([0-9]+\.[0-9]+)([Ff]{0,1})$/)
152             {
153 2 100       41 push @macros, Const::Introspect::C::Constant->new(
154             c => $self,
155             name => $name,
156             raw_value => $value,
157             value => $1,
158             type => $2 ? 'float' : 'double',
159             );
160             }
161             else
162             {
163 1         40 push @macros, Const::Introspect::C::Constant->new(
164             c => $self,
165             name => $name,
166             raw_value => $value,
167             );
168             }
169             }
170             else
171             {
172 0         0 warn "unable to parse line: $line";
173             }
174             }
175              
176 1         62 @macros;
177             }
178              
179              
180 6         33 sub get_single ($self, $name)
181 6     6 1 6718 {
  6         28  
  6         14  
182 6         623 Const::Introspect::C::Constant->new(
183             c => $self,
184             name => $name,
185             );
186             }
187              
188              
189 28         150 sub _tt ($self, $name, %args)
  28         89  
190 28     28   75 {
  28         217  
  28         98  
191 28         72 state $cache;
192              
193 28   66     243 my $template = $cache->{$name} //= do {
194 4         13 state $dss;
195 4   66     53 $dss //= Data::Section::Simple->new(__PACKAGE__);
196 4   50     76 $dss->get_data_section($name) // die "no such template: $name";
197             };
198              
199 28         1892 state $tt;
200              
201 28   66     171 $tt //= Template->new;
202              
203 28         53489 my $output = '';
204 28         263 $args{self} = $self;
205 28 50       783 $tt->process(\$template, \%args, \$output) || die $tt->error;
206 28         224568 $output;
207             }
208              
209             # give a unique name for each lib
210 28         82 sub _lib_name ($self, $name)
211 28     28   152 {
  28         78  
  28         57  
212 28         77 state $counter = 0;
213 28         75 $counter++;
214 28         176 join '', $name, $$, $counter;
215             }
216              
217 28         55 sub _build_from_template ($self, $name1, $name2, %args)
  28         123  
  28         71  
218 28     28   71 {
  28         124  
  28         58  
219 28 50       1123 my $source = Path::Tiny->tempfile(
220             TEMPLATE => "$name1-XXXXXX",
221             SUFFIX => $self->lang eq 'c' ? '.c' : '.cxx',
222             );
223 28         28576 $source->spew_utf8(
224             $self->_tt(
225             "$name1.c.tt",
226             %args,
227             )
228             );
229              
230 28         20335 my $libname = $self->_lib_name($name2);
231              
232 28         496 my $build = FFI::Build->new(
233             $libname,
234             cflags => $self->extra_cflags,
235             export => [$name1 =~ s/-/_/gr],
236             source => ["$source"],
237             );
238              
239             my($out, $lib, $error) = capture_merged {
240 28     28   35604 local $@ = '';
241 28         209 my $lib = eval { $build->build };
  28         159  
242 28         1952623 ($lib, $@)
243 28         28140 };
244              
245 28 100       33347 push $self->diag->@*, $out
246             if $out eq '';
247              
248 28 100       424 die $error if $error;
249              
250 26         339 my $ffi = FFI::Platypus->new(
251             api => 1,
252             lib => [$lib->path],
253             );
254              
255 26         68954 ($ffi, $build)
256             }
257              
258 17         35 sub compute_expression_type ($self, $expression)
259 17     17 1 8102 {
  17         36  
  17         30  
260 17         86 my($ffi, $build) = $self->_build_from_template(
261             'compute-expression-type',
262             'cet',
263             expression => $expression,
264             );
265              
266 15         3786 my $type = $ffi->function( 'compute_expression_type' => [] => 'string' )
267             ->call;
268              
269 15         8247 $build->clean;
270              
271 15         7758 $type;
272             }
273              
274              
275 11         25 sub compute_expression_value ($self, $type, $expression)
  11         22  
276 11     11 1 4023 {
  11         25  
  11         21  
277 11         39 my $ctype = $type;
278 11 100       47 $ctype = 'const char *' if $type eq 'string';
279 11 100       51 $ctype = 'void *' if $type eq 'pointer';
280 11         24 my $ffitype = $type;
281 11 100       34 $ffitype = 'opaque' if $type eq 'pointer';
282              
283 11         61 my($ffi, $build) = $self->_build_from_template(
284             'compute-expression-value',
285             'cev',
286             ctype => $ctype,
287             expression => $expression,
288             );
289              
290 11         2760 my $value = $ffi->function( 'compute_expression_value' => [] => $ffitype )
291             ->call;
292              
293 11         6849 $build->clean;
294              
295 11         5428 $value;
296             }
297              
298 2     2   21 no Moo;
  2         5  
  2         20  
299              
300              
301             1;
302              
303             =pod
304              
305             =encoding UTF-8
306              
307             =head1 NAME
308              
309             Const::Introspect::C - Find and evaluate C/C++ constants for use in Perl
310              
311             =head1 VERSION
312              
313             version 0.01
314              
315             =head1 SYNOPSIS
316              
317             use Const::Introspect::C;
318            
319             my $c = Const::Introspect::C->new(
320             headers => ['foo.h'],
321             );
322            
323             foreach my $const ($c->get_macro_constants)
324             {
325             # const isa Const::Introspect::C::Constant
326             say "name = ", $const->name;
327             # type is one of: int, long, pointer, string,
328             # float, double or "other"
329             say "type = ", $const->type;
330             say "value = ", $const->value;
331             }
332              
333             =head1 DESCRIPTION
334              
335             B: This is an early release, expect some interface changes in the near future.
336              
337             This module provides an interface for finding C/C++ constant style macros, and can
338             compute their types and values. It can also be used to compute the values of
339             enumerated type constants, although this module doesn't have a way of finding
340             the names (For that try something like L).
341              
342             =head1 PROPERTIES
343              
344             =head2 headers
345              
346             List of C/C++ header files.
347              
348             =head2 lang
349              
350             The programming language. Should be one of C or C. The default is C.
351              
352             =head2 cc
353              
354             The C compiler. The default is the C compiler used by Perl itself,
355             automatically split on the appropriate whitespace.
356             This should be a array reference, so C<['clang']> and not C<'clang'>.
357             This allows for C with spaces in it.
358              
359             =head2 ppflags
360              
361             The C pre-processor flags. This may change in the future, or on some platforms, but as of
362             this writing this is C<-dM -E -x c> for C and C<-dM -E -x c++> for C++. This must be an
363             array reference.
364              
365             =head2 cflags
366              
367             C compiler flags. This is what Perl uses by default. This must be an array reference.
368              
369             =head2 extra_cflags
370              
371             Extra Compiler flags. This is an empty array by default. This allows the caller to provide additional
372             library specific flags, like C<-I>.
373              
374             =head2 source
375              
376             C source file. This is an instance of L and it is created on-the-fly. You shouldn't
377             need to specify this explicitly.
378              
379             =head2 filter
380              
381             Filter regular expression that all macro names must match. This is C<^[^_]> by default, which means
382             all macros starting with an underscore are skipped.
383              
384             =head2 diag
385              
386             List of diagnostic failures.
387              
388             =head1 METHODS
389              
390             =head2 get_macro_constants
391              
392             my @const = $c->get_macro_constants;
393              
394             This generates the source file, runs the pre-processor, parses the macros as well as possible and
395             returns the result as a list of L instances.
396              
397             =head2 get_single
398              
399             my $const = $c->get_single($name);
400              
401             Get a single constant by the name of C<$name>. Returns an instance of
402             L. This is most useful for getting the integer
403             values for named enumerated values.
404              
405             =head2 compute_expression_type
406              
407             my $type = $c->compute_expression_type($expression);
408              
409             This attempts to compute the type of the C C<$expression>. It should
410             return one of C, C, C, C, C, or C.
411             If the type cannot be determined then C will be returned, and
412             often indicates a code macro that doesn't have a corresponding
413             constant.
414              
415             =head2 compute_expression_value
416              
417             my $value = $c->compute_expression_value($type, $expression);
418              
419             This method attempts to compute the value of the given C C<$expression> of
420             the given C<$type>. C<$type> should be one of C, C, C,
421             C, or C.
422              
423             If you do not know the expression type, you can try to compute the type
424             using C above.
425              
426             =head1 CAVEATS
427              
428             This modules requires the C pre-processor for macro constants, and for many constants
429             requires a compiler to compute the type and value. The techniques used by this module
430             work with C and C, but they probably don't work with other compilers.
431             Patches welcome to support other compilers.
432              
433             This module can tell you the value of pointer constants, but there is not much utility
434             to the value of non C values.
435              
436             =head1 AUTHOR
437              
438             Graham Ollis
439              
440             =head1 COPYRIGHT AND LICENSE
441              
442             This software is copyright (c) 2020 by Graham Ollis.
443              
444             This is free software; you can redistribute it and/or modify it under
445             the same terms as the Perl 5 programming language system itself.
446              
447             =cut
448              
449             __DATA__