[PATCH 2/2] Fortran: f_c_string intrinsic improvements

Sandra Loosemore <[email protected]>
Newsgroups gmane.comp.gcc.patches,gmane.comp.gcc.fortran
Message-ID <[email protected]>
The existing implementation of f_c_string is quite inefficient, doing
either 2 or 3 allocations and copies of the input string prefix.  This
rewrite adds folding for constant string arguments and handles other
cases with a single allocation and copy.

This patch also adds the missing documentation for this intrinsic to the
gfortran manual.

gcc/fortran/ChangeLog
	* intrinsic.texi (F_C_STRING): New section.
	* trans-intrinsic.cc (conv_trim): Delete.
	(conv_isocbinding_function): Rewrite the F_C_STRING case.

gcc/testsuite/ChangeLog
	* gfortran.dg/f_c_string3.f90: New.
	* gfortran.dg/f_c_string4.f90: New.
	* gfortran.dg/f_c_string5.f90: New.
---
 gcc/fortran/intrinsic.texi                |  57 ++++
 gcc/fortran/trans-intrinsic.cc            | 328 ++++++++++++----------
 gcc/testsuite/gfortran.dg/f_c_string3.f90 |  53 ++++
 gcc/testsuite/gfortran.dg/f_c_string4.f90 |  26 ++
 gcc/testsuite/gfortran.dg/f_c_string5.f90 |  20 ++
 5 files changed, 328 insertions(+), 156 deletions(-)
 create mode 100644 gcc/testsuite/gfortran.dg/f_c_string3.f90
 create mode 100644 gcc/testsuite/gfortran.dg/f_c_string4.f90
 create mode 100644 gcc/testsuite/gfortran.dg/f_c_string5.f90

diff --git a/gcc/fortran/intrinsic.texi b/gcc/fortran/intrinsic.texi
index 8a33fff68f9..699b38e157c 100644
--- a/gcc/fortran/intrinsic.texi
+++ b/gcc/fortran/intrinsic.texi
@@ -159,6 +159,7 @@ Some basic guidelines for editing this document:
 * @code{EXP}:           EXP,       Exponential function
 * @code{EXPONENT}:      EXPONENT,  Exponent function
 * @code{EXTENDS_TYPE_OF}: EXTENDS_TYPE_OF,  Query dynamic type for extension
+* @code{F_C_STRING}:    F_C_STRING,  Convert character scalar to C string
 * @code{FDATE}:         FDATE,     Subroutine (or function) to get the current time as a string
 * @code{FGET}:          FGET,      Read a single character in stream mode from stdin
 * @code{FGETC}:         FGETC,     Read a single character in stream mode
@@ -3558,6 +3559,7 @@ Fortran 2023 and later.
 @ref{C_LOC}, @*
 @ref{C_F_POINTER}
 @ref{C_F_PROCPOINTER}
+@ref{F_C_STRING}
 @end table
 
 
@@ -6394,6 +6396,61 @@ Fortran 2003 and later
 
 
 
+@node F_C_STRING
+@section @code{F_C_STRING} --- Convert Fortran character scalar to C string
+@fnindex F_C_STRING
+@cindex string, convert Fortran to C
+
+@table @asis
+@item @emph{Synopsis}:
+@code{RESULT = F_C_STRING(STRING[, ASIS])}
+
+@item @emph{Description}:
+The @code{F_C_STRING} intrinsic is equivalent to @code{STRING//C_NULL_CHAR}
+if the @code{ASIS} argument is present and true, and to
+@code{TRIM(STRING)//C_NULL_CHAR} otherwise.
+
+@item @emph{Class}:
+Transformational function
+
+@item @emph{Arguments}:
+@multitable @columnfractions .15 .70
+@item @var{STRING}  @tab A character scalar of kind @code{C_CHAR}.
+@item @var{ASIS}    @tab An optional logical scalar.
+@end multitable
+
+@item @emph{Return value}:
+The result is a null-terminated character scalar of the same type and kind
+as @code{STRING}, suitable for passing to a C function that accepts a
+@code{char *} argument.
+
+@item @emph{Example}:
+@smallexample
+program main
+  use iso_c_binding, only: f_c_string, c_char
+  implicit none (external, type)
+  character(:, c_char), allocatable :: s1, s2, s3
+
+  ! s1 is null-terminated "hello, world!   "
+  s1 = f_c_string ("hello, world!   ", .true.)
+
+  ! s2 is null-terminated "hello, world!"
+  s2 = f_c_string ("hello, world!   ", .false.)
+
+  ! s3 is null-terminated "hello, world!" (same as s2 example)
+  s3 = f_c_string ("hello, world!   ")
+end program main
+
+@end smallexample
+
+@item @emph{Standard}:
+Fortran 2023 and later.
+
+@item @emph{See also}:
+@ref{C_F_STRPOINTER}
+@end table
+
+
 @node FDATE
 @section @code{FDATE} --- Get the current time as a string
 @fnindex FDATE
diff --git a/gcc/fortran/trans-intrinsic.cc b/gcc/fortran/trans-intrinsic.cc
index a18a6436062..fdb9ddb52ea 100644
--- a/gcc/fortran/trans-intrinsic.cc
+++ b/gcc/fortran/trans-intrinsic.cc
@@ -9840,37 +9840,6 @@ gfc_conv_intrinsic_loc (gfc_se * se, gfc_expr * expr)
   se->expr = temp_var;
 }
 
-
-/* Specialized trim for f_c_string.  */
-
-static void
-conv_trim (gfc_se *tse, gfc_se *str)
-{
-  tree cond, plen, pvar, tlen, ttmp, tvar;
-
-  tlen = gfc_create_var (gfc_charlen_type_node, "tlen");
-  plen = gfc_build_addr_expr (NULL_TREE, tlen);
-
-  tvar = gfc_create_var (pchar_type_node, "tstr");
-  pvar = gfc_build_addr_expr (ppvoid_type_node, tvar);
-
-  ttmp = build_call_expr_loc (input_location, gfor_fndecl_string_trim, 4,
-			      plen, pvar, str->string_length, str->expr);
-
-  gfc_add_expr_to_block (&tse->pre, ttmp);
-
-  /* Free the temporary afterwards, if necessary.  */
-  cond = fold_build2_loc (input_location, GT_EXPR, logical_type_node,
-			  tlen, build_int_cst (TREE_TYPE (tlen), 0));
-  ttmp = gfc_call_free (tvar);
-  ttmp = build3_v (COND_EXPR, cond, ttmp, build_empty_stmt (input_location));
-  gfc_add_expr_to_block (&tse->post, ttmp);
-
-  tse->expr = tvar;
-  tse->string_length = tlen;
-}
-
-
 /* The following routine generates code for the intrinsic functions from
    the ISO_C_BINDING module: C_LOC, C_FUNLOC, C_ASSOCIATED, and
    F_C_STRING.  */
@@ -9965,141 +9934,188 @@ conv_isocbinding_function (gfc_se *se, gfc_expr *expr)
 	 f_c_string(string, .false.) -> trim(string) // c_null_char
 	 f_c_string(string, .true.)  -> string       // c_null_char  */
 
-      gfc_se lse, rse, tse;
-      tree len, tmp, var;
       gfc_expr *string = arg->expr;
       gfc_expr *asis = arg->next->expr;
-      gfc_expr *cnc;
+      bool need_asis = false, need_trim = false;
+      gfc_se asis_se;
 
-      /* Convert string. */
-      gfc_init_se (&lse, se);
-      gfc_conv_expr (&lse, string);
-      gfc_conv_string_parameter (&lse);
-
-      /* Create a string for C_NULL_CHAR and convert it.  */
-      cnc = gfc_get_character_expr (gfc_default_character_kind,
-				    &string->where, "\0", 1);
-      gfc_init_se (&rse, se);
-      gfc_conv_expr (&rse, cnc);
-      gfc_conv_string_parameter (&rse);
-      gfc_free_expr (cnc);
-
-#ifdef cnode
-#undef cnode
-#endif
-#define cnode gfc_charlen_type_node
-      if (asis)
+      if (!asis)
 	{
-	  stmtblock_t block;
-	  gfc_se asis_se, vse;
-	  tree elen, evar, tlen, tvar;
-	  tree else_branch, then_branch;
-
-	  elen = evar = tlen = tvar = NULL_TREE;
-
-	  /* f_c_string(string, .true.) -> string // c_null_char  */
-
-	  gfc_init_block (&block);
-
-	  gfc_add_block_to_block (&block, &lse.pre);
-	  gfc_add_block_to_block (&block, &rse.pre);
-
-	  tlen = fold_build2_loc (input_location, PLUS_EXPR, cnode,
-				  fold_convert (cnode, lse.string_length),
-				  fold_convert (cnode, rse.string_length));
-
-	  gfc_init_se (&vse, se);
-	  tvar = gfc_conv_string_tmp (&vse, pchar_type_node, tlen);
-	  gfc_add_block_to_block (&block, &vse.pre);
-
-	  tmp = build_call_expr_loc (input_location, gfor_fndecl_concat_string,
-				     6, tlen, tvar,
-				     lse.string_length, lse.expr,
-				     rse.string_length, rse.expr);
-	  gfc_add_expr_to_block (&block, tmp);
-
-	  then_branch = gfc_finish_block (&block);
-
-	  /* f_c_string(string, .false.) = trim(string) // c_null_char  */
-
-	  gfc_init_block (&block);
-
-	  gfc_init_se (&tse, se);
-	  conv_trim (&tse, &lse);
-	  gfc_add_block_to_block (&block, &tse.pre);
-	  gfc_add_block_to_block (&block, &rse.pre);
-
-	  elen = fold_build2_loc (input_location, PLUS_EXPR, cnode,
-				  fold_convert (cnode, tse.string_length),
-				  fold_convert (cnode, rse.string_length));
-
-	  gfc_init_se (&vse, se);
-	  evar = gfc_conv_string_tmp (&vse, pchar_type_node, elen);
-	  gfc_add_block_to_block (&block, &vse.pre);
-
-	  tmp = build_call_expr_loc (input_location, gfor_fndecl_concat_string,
-				     6, elen, evar,
-				     tse.string_length, tse.expr,
-				     rse.string_length, rse.expr);
-	  gfc_add_expr_to_block (&block, tmp);
-
-	  else_branch = gfc_finish_block (&block);
-
-	  gfc_init_se (&asis_se, se);
-	  gfc_conv_expr (&asis_se, asis);
-	  if (asis->expr_type == EXPR_VARIABLE
-	    && asis->symtree->n.sym->attr.dummy
-	    && asis->symtree->n.sym->attr.optional)
-	    {
-	      tree present = gfc_conv_expr_present (asis->symtree->n.sym);
-	      asis_se.expr = build3_loc (input_location, COND_EXPR,
-					 logical_type_node, present,
-					 asis_se.expr,
-					 build_int_cst (logical_type_node, 0));
-	    }
-	  gfc_add_block_to_block (&se->pre, &asis_se.pre);
-	  tmp = fold_build3_loc (input_location, COND_EXPR, void_type_node,
-				 asis_se.expr, then_branch, else_branch);
-
-	  gfc_add_expr_to_block (&se->pre, tmp);
-
-	  var = fold_build3_loc (input_location, COND_EXPR, pchar_type_node,
-				 asis_se.expr, tvar, evar);
-	  gfc_add_expr_to_block (&se->pre, var);
-
-	  len = fold_build3_loc (input_location, COND_EXPR, cnode,
-				 asis_se.expr, tlen, elen);
-	  gfc_add_expr_to_block (&se->pre, len);
+	  need_trim = true;
+	  need_asis = false;
+	}
+      else if (asis->expr_type == EXPR_CONSTANT)
+	{
+	  need_asis = asis->value.logical;
+	  need_trim = !need_asis;
 	}
       else
 	{
-	  /* f_c_string(string) = trim(string) // c_null_char  */
-
-	  gfc_add_block_to_block (&se->pre, &lse.pre);
-	  gfc_add_block_to_block (&se->pre, &rse.pre);
-
-	  gfc_init_se (&tse, se);
-	  conv_trim (&tse, &lse);
-	  gfc_add_block_to_block (&se->pre, &tse.pre);
-	  gfc_add_block_to_block (&se->post, &tse.post);
-
-	  len = fold_build2_loc (input_location, PLUS_EXPR, cnode,
-				 fold_convert (cnode, tse.string_length),
-				 fold_convert (cnode, rse.string_length));
-
-	  var = gfc_conv_string_tmp (se, pchar_type_node, len);
-
-	  tmp = build_call_expr_loc (input_location, gfor_fndecl_concat_string,
-				     6, len, var,
-				     tse.string_length, tse.expr,
-				     rse.string_length, rse.expr);
-	  gfc_add_expr_to_block (&se->pre, tmp);
+	  /* A conditional expression is needed.  */
+	  need_asis = true;
+	  need_trim = true;
+	  gfc_init_se (&asis_se, se);
+	  gfc_conv_expr (&asis_se, asis);
+	  if (asis->expr_type == EXPR_VARIABLE
+	      && asis->symtree->n.sym->attr.dummy
+	      && asis->symtree->n.sym->attr.optional)
+	    {
+	      tree present = gfc_conv_expr_present (asis->symtree->n.sym);
+	      asis_se.expr
+		= build3_loc (input_location, COND_EXPR,
+			      logical_type_node, present,
+			      asis_se.expr, logical_false_node);
+	    }
+	  gfc_make_safe_expr (&asis_se);
 	}
 
-      se->expr = var;
-      se->string_length = len;
+      /* Handle the case of a constant string argument first.  */
+      if (string->expr_type == EXPR_CONSTANT)
+	{
+	  /* Output for the asis "then" case goes tlen/tstr, and the
+	     trimmed case in elen/estr.  */
+	  tree elen, estr, tlen, tstr;
+	  elen = estr = tlen = tstr = NULL_TREE;
 
-#undef cnode
+	  gfc_char_t *orig_string = string->value.character.string;
+	  gfc_charlen_t orig_len = string->value.character.length;
+	  gfc_charlen_t n;
+	  gfc_char_t *buf
+	    = (gfc_char_t *) alloca ((orig_len + 1) * sizeof (gfc_char_t));
+	  memcpy (buf, orig_string, orig_len * sizeof (gfc_char_t));
+	  buf[orig_len] = '\0';
+	  int kind = gfc_default_character_kind;
+	  gcc_assert (string->ts.kind == kind);
+
+	  /* Build the new string constant(s).  */
+	  if (need_asis)
+	    {
+	      tstr = gfc_build_wide_string_const (kind, orig_len + 1, buf);
+	      tlen = TYPE_MAX_VALUE (TYPE_DOMAIN (TREE_TYPE (tstr)));
+	      if (!need_trim)
+		{
+		  se->expr = tstr;
+		  se->string_length = tlen;
+		  return;
+		}
+	    }
+	  if (need_trim)
+	    {
+	      for (n = orig_len; n; n--)
+		if (buf[n - 1] != ' ')
+		  break;
+	      buf[n] = '\0';
+	      if (need_asis && n == orig_len)
+		{
+		  /* Special case; trimming is a no-op.  Add side-effects
+		     from the condition and then just return the string
+		     without a conditional.  */
+		  gfc_add_block_to_block (&se->pre, &asis_se.pre);
+		  se->expr = tstr;
+		  se->string_length = tlen;
+		  return;
+		}
+	      else
+		{
+		  estr = gfc_build_wide_string_const (kind, n + 1, buf);
+		  elen = TYPE_MAX_VALUE (TYPE_DOMAIN (TREE_TYPE (estr)));
+		}
+	      if (!need_asis)
+		{
+		  se->expr = estr;
+		  se->string_length = elen;
+		  return;
+		}
+	    }
+	  gcc_assert (need_asis && need_trim);
+	  gfc_add_block_to_block (&se->pre, &asis_se.pre);
+	  se->expr
+	    = fold_build3_loc (input_location, COND_EXPR,
+			       pchar_type_node, asis_se.expr,
+			       tstr, estr);
+	  se->string_length
+	    = fold_build3_loc (input_location, COND_EXPR,
+			       gfc_charlen_type_node, asis_se.expr,
+			       tlen, elen);
+	  return;
+	}
+      else
+	/* We have to generate code to do the string transformation(s) at
+	   runtime.  */
+	{
+	  tree tmp;
+
+	  /* Convert input string. */
+	  gfc_se sse;
+	  gfc_init_se (&sse, se);
+	  gfc_conv_expr (&sse, string);
+	  gfc_conv_string_parameter (&sse);
+	  gfc_make_safe_expr (&sse);
+	  gfc_add_block_to_block (&se->pre, &sse.pre);
+
+	  /* Use a temporary for the (possibly trimmed) string length.  */
+	  tree lenvar = gfc_create_var (gfc_charlen_type_node, NULL);
+	  gfc_add_modify (&se->pre, lenvar, sse.string_length);
+
+	  /* Build the expression for a call to LEN_TRIM if we may need
+	     to trim the string.  If it's conditional, handle that too.  */
+	  if (need_trim)
+	    {
+	      tree trimlen
+		= build_call_expr_loc (input_location,
+				       gfor_fndecl_string_len_trim, 2,
+				       lenvar, sse.expr);
+	      if (need_asis)
+		{
+		  gfc_add_block_to_block (&se->pre, &asis_se.pre);
+		  tmp = fold_build3_loc (input_location, COND_EXPR,
+					 gfc_charlen_type_node, asis_se.expr,
+					 lenvar, trimlen);
+		  gfc_add_modify (&se->pre, lenvar, tmp);
+		}
+	      else
+		gfc_add_modify (&se->pre, lenvar, trimlen);
+	    }
+
+	  /* Allocate a new string newvar that is lenvar+1 bytes long.
+	     memcpy the first lenvar bytes from the input string, and
+	     add a null character.  Note that lenvar, the length of
+	     the (trimmed) original string, has type gfc_charlen_type_node,
+	     but newlen is size_type_node.  */
+	  tree string_type_node = build_pointer_type (char_type_node);
+	  tree newvar = gfc_create_var (string_type_node, NULL);
+	  tree newlen = fold_build2_loc (input_location, PLUS_EXPR,
+					 size_type_node,
+					 fold_convert (size_type_node,
+						       lenvar),
+					 size_one_node);
+	  gfc_add_modify (&se->pre, newvar,
+			  gfc_call_malloc (&se->pre, string_type_node,
+					   newlen));
+	  tmp = build_call_expr_loc (input_location,
+				     builtin_decl_explicit (BUILT_IN_MEMCPY),
+				     3,
+				     fold_convert (pvoid_type_node, newvar),
+				     fold_convert (pvoid_type_node, sse.expr),
+				     fold_convert (size_type_node, lenvar));
+	  gfc_add_expr_to_block (&se->pre, tmp);
+	  tmp = fold_build2_loc (input_location, POINTER_PLUS_EXPR,
+				 string_type_node, newvar,
+				 fold_convert (size_type_node, lenvar));
+	  tmp = fold_build1_loc (input_location, INDIRECT_REF,
+				 char_type_node, tmp);
+	  gfc_add_modify (&se->pre, tmp,
+			  fold_convert (char_type_node, integer_zero_node));
+
+	  /* Remember to free the string later.  */
+	  tmp = gfc_call_free (newvar);
+	  gfc_add_expr_to_block (&se->post, tmp);
+
+	  /* Return the result.  */
+	  se->expr = newvar;
+	  se->string_length = fold_convert (gfc_charlen_type_node, newlen);
+	  return;
+	}
     }
   else
     gcc_unreachable ();
diff --git a/gcc/testsuite/gfortran.dg/f_c_string3.f90 b/gcc/testsuite/gfortran.dg/f_c_string3.f90
new file mode 100644
index 00000000000..3e9d4a79d3b
--- /dev/null
+++ b/gcc/testsuite/gfortran.dg/f_c_string3.f90
@@ -0,0 +1,53 @@
+! Test f_c_string cases that can be fully constant-folded
+! { dg-do compile }
+! { dg-additional-options "-fdump-tree-original" }
+
+use iso_c_binding, only: f_c_string, c_char
+implicit none (external, type)
+character(*, c_char), parameter :: str1 = "blah1"
+character(*, c_char), parameter :: str2 = "blah2"
+character(*, c_char), parameter :: str3 = "blah3"
+character(*, c_char), parameter :: str4 = "blah4   "
+character(*, c_char), parameter :: str5 = "blah5   "
+character(*, c_char), parameter :: str6 = "blah6   "
+external foo
+
+call foo(f_c_string("hello world1", asis=.true.))
+! { dg-final { scan-tree-dump-times "hello world1.\[^\\n\\r\]*, 13" 1 "original" } }
+
+call foo(f_c_string("hello world2", asis=.false.))
+! { dg-final { scan-tree-dump-times "hello world2.\[^\\n\\r\]*, 13" 1 "original" } }
+
+call foo(f_c_string("hello world3"))
+! { dg-final { scan-tree-dump-times "hello world3.\[^\\n\\r\]*, 13" 1 "original" } }
+
+call foo(f_c_string("hello1 ", asis=.true.))
+! { dg-final { scan-tree-dump-times "hello1 .\[^\\n\\r\]*, 8" 1 "original" } }
+
+call foo(f_c_string("hello2 ", asis=.false.))
+! { dg-final { scan-tree-dump-times "hello2.\[^\\n\\r\]*, 7" 1 "original" } }
+
+call foo(f_c_string("hello3 "))
+! { dg-final { scan-tree-dump-times "hello3.\[^\\n\\r\]*, 7" 1 "original" } }
+
+call foo(f_c_string(str1, asis=.true.))
+! { dg-final { scan-tree-dump-times "blah1.\[^\\n\\r\]*, 6" 1 "original" } }
+
+call foo(f_c_string(str2, asis=.false.))
+! { dg-final { scan-tree-dump-times "blah2.\[^\\n\\r\]*, 6" 1 "original" } }
+
+call foo(f_c_string(str3))
+! { dg-final { scan-tree-dump-times "blah3.\[^\\n\\r\]*, 6" 1 "original" } }
+
+call foo(f_c_string(str4, asis=.true.))
+! { dg-final { scan-tree-dump-times "blah4   .\[^\\n\\r\]*, 9" 1 "original" } }
+
+call foo(f_c_string(str5, asis=.false.))
+! { dg-final { scan-tree-dump-times "blah5.\[^\\n\\r\]*, 6" 1 "original" } }
+
+call foo(f_c_string(str6))
+! { dg-final { scan-tree-dump-times "blah6.\[^\\n\\r\]*, 6" 1 "original" } }
+
+end
+
+
diff --git a/gcc/testsuite/gfortran.dg/f_c_string4.f90 b/gcc/testsuite/gfortran.dg/f_c_string4.f90
new file mode 100644
index 00000000000..d38e16f0268
--- /dev/null
+++ b/gcc/testsuite/gfortran.dg/f_c_string4.f90
@@ -0,0 +1,26 @@
+! Test f_c_string cases with constant strings but that need a conditional.
+! { dg-do compile }
+! { dg-additional-options "-fdump-tree-original" }
+
+subroutine doit (x)
+
+use iso_c_binding, only: f_c_string, c_char
+implicit none (external, type)
+logical :: x
+character(*, c_char), parameter :: str1 = "blah1"
+character(*, c_char), parameter :: str2 = "blah2   "
+external foo
+
+call foo(f_c_string("hello world1", asis=x))
+! { dg-final { scan-tree-dump-times "hello world1.\[^\\n\\r\]*, 13" 1 "original" } }
+
+call foo(f_c_string("hello1 ", asis=x))
+! { dg-final { scan-tree-dump-times "hello1 .\[^\\n\\r\]* 8 : 7" 1 "original" } }
+
+call foo(f_c_string(str1, asis=x))
+! { dg-final { scan-tree-dump-times "blah1.\[^\\n\\r\]*, 6" 1 "original" } }
+
+call foo(f_c_string(str2, asis=x))
+! { dg-final { scan-tree-dump-times "blah2   .\[^\\n\\r\]* 9 : 6" 1 "original" } }
+
+end subroutine
diff --git a/gcc/testsuite/gfortran.dg/f_c_string5.f90 b/gcc/testsuite/gfortran.dg/f_c_string5.f90
new file mode 100644
index 00000000000..25c5115f214
--- /dev/null
+++ b/gcc/testsuite/gfortran.dg/f_c_string5.f90
@@ -0,0 +1,20 @@
+! { dg-do run }
+! This is the example from the manual.
+
+program main
+  use iso_c_binding, only: f_c_string, c_char
+  implicit none (external, type)
+  character(:, c_char), allocatable :: s1, s2, s3
+
+  ! s1 is null-terminated "hello, world!   "
+  s1 = f_c_string ("hello, world!   ", .true.)
+  if (len(s1) .ne. 17) stop 100
+
+  ! s2 is null-terminated "hello, world!"
+  s2 = f_c_string ("hello, world!   ", .false.)
+  if (len(s2) .ne. 14) stop 200
+
+  ! s3 is null-terminated "hello, world!" (same as s2 example)
+  s3 = f_c_string ("hello, world!   ")
+  if (len(s3) .ne. 14) stop 200
+end program main
-- 
2.39.5
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.