Rupert  Beatty

Rupert Beatty

1672445340

Purely Native, Extensible Rich Text Editor for iOS, MacOS Catalyst App

Note: While Proton is already a very powerful and flexible framework, it is still in early stages of development. The APIs and public interfaces are still undergoing revisions and may introduce breaking changes with every version bump before reaching stable version 1.0.0.

Proton is a simple library that allows you to extend the behavior of a textview to add rich content that you always wanted. It provides simple API that allows you to extend the textView to include complex content like nested textViews or for that matter, any other UIView. In the simplest terms - It's what you always wanted UITextView to be.

Proton is designed keeping the following requirements in mind:

  • Be a standalone component with nothing that is specific to anything that is required in complex Text Editor. At it's most basic form, it should be able to be used as a UITextView and in most complex form, it should be able to provide rich text editing capabilities which are beyond just text formatting.
  • Should be extensible to support adding any view as content in the Editor such that it flows with the text.
  • Resizing of content views should automatically resize the containing Editor and support this to nth nesting level.
  • Should support extending the appearance of text as the content is typed - for e.g. changing text as it is typed using mark-up syntax and yet, not be aware of any of these requirements directly.
  • Should allow for working on multiple editors through the same toolbar based on where the focus is, and yet not be aware of the toolbar itself.
  • Respect the bounds of the container i.e. resize to change bounds when the device orientation changes.
  • Support a default font and styling like alignment and head indentation.
  • And of course, support all this on macOS Catalyst as well with almost no additional effort.

Core Concepts

At it's core, Proton constitutes of following key components:

  • EditorView: A substitute for UITextView that can be extended to add custom views including other EditorViews.
  • TextProcessor: Allows you to inject a behavior that is invoked as you type text in the EditorView. This can be used to change text, add/remove attributes like color or replace the added text with an entirely different text/view. For e.g. as you type markup syntax, you can convert the markup text into a formatted text by adding corresponding behavior to the TextProcessor.
  • EditorCommand: Allows you to add a behavior that can be invoked on demand on the given EditorView. For e.g. selecting some text and making it bold.
  • Attachment: A container capable of hosting a custom view including another EditorView. Attachment is a supercharged NSTextAttachment that can have automatic constraints applied on it to size it in various configurations like matching content, range of width, fixed width and so on. It also has helper functions to get it's range in it's container as well as to remove itself from the container.

A practical use case

The power of EditorView to host rich content is made possible by the use of Attachment which allows hosting any UIView in the EditorView. This is further enhanced by use of TextProcessor and EditorCommand to add interactive behavior to the editing experience.

Let's take an example of a Panel and see how that can be created in the EditorView. Following are the key requirements for a Panel:

  1. A text block that is indented and has a custom UI besides the Editor.
  2. Change height based on the content being typed.
  3. Have a different font color than the main text.
  4. Able to be inserted using a button.
  5. Able to be inserted by selecting text and clicking a button.
  6. Able to be inserted in a given Editor by use of >> char.
  7. Nice to have: delete using backspace key when empty similar to a Blockquote.

Panel view

The first thing that is required is to create a view that represents the Panel. Once we have created this view, we can add it to an attachment and insert it in the EditorView.

extension EditorContent.Name {
    static let panel = EditorContent.Name("panel")
}
class PanelView: UIView, BlockContent, EditorContentView {
    let container = UIView()
    let editor: EditorView
    let iconView = UIImageView()    
    var name: EditorContent.Name {
        return .panel
    }   
    override init(frame: CGRect) {
        self.editor = EditorView(frame: frame)
        super.init(frame: frame)    
        setup()
    }   
    var textColor: UIColor {
        get { editor.textColor }
        set { editor.textColor = newValue }
    }   
    override var backgroundColor: UIColor? {
        get { container.backgroundColor }
        set {
            container.backgroundColor = newValue
            editor.backgroundColor = newValue
        }
    }   
    private func setup() {
        // setup view by creating required constraints
    }
}

As the Panel contains an Editor inside itself, the height will automatically change based on the content as it is typed in. To restrict the height to a given maximum value, an absolute size or autolayout constraint may be used.

Using the textColor property, the default font color may be changed.

For the ability to add Panel to the Editor using a button, we can make use of EditorCommand. A Command can be executed on a given EditorView or via CommandExecutor that automatically takes care of executing the command on the focussed EditorView. To insert an EditorView inside another, we need to first create an Attachment and then used a Command to add to the desired position:

class PanelAttachment: Attachment {
    var view: PanelView 
    init(frame: CGRect) {
        view = PanelView(frame: frame)
        super.init(view, size: .fullWidth)
        view.delegate = self
        view.boundsObserver = self
    }   
    var attributedText: NSAttributedString {
        get { view.attributedText }
        set { view.attributedText = newValue }
    }   
}   
class PanelCommand: EditorCommand {
    func execute(on editor: EditorView) {
        let selectedText = editor.selectedText  
        let attachment = PanelAttachment(frame: .zero)
        attachment.selectBeforeDelete = true
        editor.insertAttachment(in: editor.selectedRange, attachment: attachment)   
        let panel = attachment.view
        panel.editor.maxHeight = 300
        panel.editor.replaceCharacters(in: .zero, with: selectedText)
        panel.editor.selectedRange = panel.editor.textEndRange
    }
}

The code in PanelCommand.execute reads the selectedText from editor and sets it back in panel.editor. This makes it possible to take the selected text from main editor, wrap it in a panel and then insert the panel in the main editor replacing the selected text.

To allow insertion of a Panel using a shortcut text input instead of clicking a button, you can use a TextProcessor:

class PanelTextProcessor: TextProcessing {  
 private let trigger = ">> "
 var name: String {
     return "PanelTextProcessor"
 }  
 var priority: TextProcessingPriority {
     return .medium
 }  
 func process(editor: EditorView, range editedRange: NSRange, changeInLength delta: Int, processed: inout Bool) {
     let line = editor.currentLine
     guard line.text.string == trigger else {
         return
     }
     let attachment = PanelAttachment(frame: .zero)
     attachment.selectBeforeDelete = true        
     editor.insertAttachment(in: line.range, attachment: attachment)
 }

For a requirement like deleting the Panel when backspace is tapped at index 0 on an empty Panel, EdtiorViewDelegate may be utilized:

In the code above, PanelViewDelegate is acting as a passthrough for EditorViewDelegate for the Editor inside the PanelView.

Checkout the complete code in the ExamplesApp.

extension PanelAttachment: PanelViewDelegate {

func panel(_ panel: PanelView, didReceiveKey key: EditorKey, at range: NSRange, handled: inout Bool) {
    if key == .backspace, range == .zero, panel.editor.attributedText.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
        removeFromContainer()
        handled = true
        }
    }
}    

Example usages

Changing text as it is typed using custom TextProcessor:

  1. Markup text processor

Adding attributes as it is typed using custom TextProcessor:

  1. Mentions text processor

Nested editors

  1. Nested editors

Panel from existing text:

  1. Panel from text

Relaying attributes to editor contained in an attachment:

  1. Relay attributes

Highlighting using custom command in Editor:

  1. Highlight in Renderer

Find text and scroll in Editor:

  1. Find in Renderer

Learn more

  • Proton API reference is available here.
  • For sample code, including the ones for examples shown above, please refer to the Example app.

Questions and feature requests

Feel free to create issues in github should you have any questions or feature requests. While Proton is created as a side project, I'll endeavour to respond to your issues at earliest possible.

Download Details:

Author: Rajdeep
Source Code: https://github.com/rajdeep/proton 
License: View license

#swift #ios #native #editor 

Purely Native, Extensible Rich Text Editor for iOS, MacOS Catalyst App
Gordon  Murray

Gordon Murray

1671698902

How to Write Once, Deploy Everywhere: When to Go Native?

Writing multiple native applications for different mobile platforms can be a daunting task, but taking a non-native approach comes with pitfalls of its own. In this article, Toptal Mobile Developer Joel Frank outlines the concept of Write Once, Deploy Everywhere (WODE) and explains how to choose the right development path for your project.

Write Once, Deploy Everywhere (WODE) has been the promise of many development frameworks over the past decade, whose goal is to ease the pain of writing multiple native applications. Determining which path to take is one of the most critical decisions a project manager has to make due to its high startup cost, impact upon the development team, and potential infeasibility to reverse.

Hybrid solutions such as Ionic leverage web technologies to render applications across platforms, but often, the end product falls short of a user’s expectations of a native look and feel.

However, even the term “native” has recently been muddied by frameworks that compile down to native code (e.g., React Native, Xamarin, etc.).

This article breaks down the pros and cons of various mobile development paths and weighs them against team makeup, cost, and user experience in an effort to empower product managers to make a more informed decision.

Write Once, Deploy Everywhere

The concept of Write Once, Deploy Everywhere refers to the ability of a development team to write an application once—using a single development stack, abstract of the platform(s) upon which the application will be deployed—and yet maintain the ability to deploy the application to all desired platforms, e.g., Android, iOS, Windows, etc. Ideally, this is accomplished without sacrificing maintainability, performance, or user experience (UX).

The alternative - and historical - method of mobile application development involves the straightforward process of simply writing a separate application for each platform, which, of course, carries with it its own potential high cost of time and resources.

In general, the factors to consider when choosing a development path include:

  • Age of the project
  • Makeup and size of the development team
  • Desired platform(s) for distribution
  • Required timeline to market
  • Available bandwidth to change to another path if one must

Unfortunately, applying each of these factors to each of the available paths, as well as wading through the myriad of available opinions on the subject, can be quite daunting. Furthermore, this process often leaves the project manager with a sense of uncertainty as to which path is best in order to meet the application’s requirements.

At a high level, the different mobile development paths can be put into two categories: native or WODE, i.e., native or everything else. Put simply, one either writes a native application or one doesn’t. The WODE category is further broken down into two groups:

  • Hybrid frameworks - those that leverage web technologies to render applications across multiple platforms.
  • Non-hybrid frameworks - those that use native UI components (e.g., buttons, text fields, and even layout managers) instead of rendering a web view inside of an application as hybrid frameworks do.

The majority of WODE frameworks are hybrid; however, in order to improve both performance and UX limitations of hybrid frameworks while still providing the benefits of a WODE framework, the current trend is toward non-hybrid. Due to this trend, frameworks such as React Native, Xamarin, and Appcelerator are gaining popularity.

Each of these paths—native, hybrid, and non-hybrid—has differing strengths and weaknesses, and as a result, each has different use cases for which it is most suited. The rest of this article breaks down the pros and cons of each mobile development path when considering competing priorities such as team makeup, project cost, and UX. With the exception of a few specialized use cases, writing native applications maximizes user experience at a slightly higher cost.

In general, the adage “you get what you pay for” applies, so if cost matters more than the customers’ experience, native may not be the right choice. However, once UX becomes vital, native applications become the clear choice since, in order to improve the UX, WODE-based applications incur a considerable cost in the form of either time or native expertise, which defeats the purpose of choosing a non-native development path in the first place.

Furthermore, even if that cost is paid, the WODE-based end product will always deliver an inferior UX when compared to its native counterpart. As a result, native is almost always the right choice for most development teams and for most projects.

Native Applications

Native applications are written in the core language of the given platform. For example, Android applications are written in Java, while iOS applications are written in either Obj-C or Swift. They require the development engineer to understand the language as well as the platform-specific nuances, which include, for example, third-party package integration, layout management, operating system (OS) interaction, and so on.

Pros

Highly customizable. Since each application is written utilizing native components, the only limitation to customization is the interface to the underlying frameworks, and sometimes not even then. As most native development engineers will attest, there is often a way to accomplish a given task in spite of a limited interface.

A simple proof of this idea can be found by browsing the support communities for a given platform. One will find numerous examples of how to accomplish a task that might be “off the reservation,” despite the limitations of the underlying frameworks.

A concrete iOS example of such a seemingly simple task might be to show a fullscreen overlay, above all external UI elements, e.g., a tab bar, navigation bar, etc. As shown in Figure 1, this is normally outside of the scope of the normal UI layer currently being presented. As such, in order to have a fullscreen overlay, it must be added to the hidden layer above the tab bar in the view stack. This kind of customization is typically only possible on native applications.

 

iOS TabBar layering example

Figure 1: iOS TabBar layering example.

 

Highest performance. As expected, a native application sets the benchmark for performance. Since most other framework types add one or more intermediate layers, they inherently run more slowly than a native application.

Most maintainable. Operating systems change constantly. Period. When they do, depending on whether breaking changes were made, an application must be updated in a timely fashion so as not to lose the part of one’s user base that upgrades to the newer OS. Obviously, the less time that passes between when the change is made available to one’s users and an application is updated, the better. This time is minimized when there are no dependencies that need to be updated in order to support this new OS, which is the case when working on a native application.

Cons

Additional resources. When writing applications for multiple platforms, a development team typically consists of one or more mobile software engineers for each supported platform. This, of course, inherently increases the size and cost of a development team. It also requires the team of engineers to have a variety of skills, as opposed to having a homogenous skill base. This has the potential to fragment a team with regard to support and collaboration.

Slower development cycle. Native apps have the potential to have a slower development cycle simply because a separate application must be written for each desired platform. The extreme case is when there is a single mobile development engineer on the team since each application is essentially written in series.

Low performance. It might seem odd to have performance as both a pro and a con. On one hand, native applications give the developer enough room to create a finely tuned, high-performance application. On the other hand, however, they also give the developer enough rope to hang themselves. If they don’t know what they are doing, in the end, they will end up with a subpar application at best.

Note: In general, this applies to all framework paths (native, hybrid, and non-hybrid). If the engineers developing an application have insufficient skills for what they are attempting, the resulting application will likely neither meet design requirements nor be well accepted by users.

Hybrid Applications

Hybrid applications are typically written using HTML/CSS/LESS to design the user interface: the “V” in the MVC design pattern. The “C,” or controller, is then typically written in JavaScript - ideally, using a JavaScript MVC framework such as AngularJS. The use of a framework like AngularJS allows for a better separation of classes and responsibilities than is typically possible using only jQuery.

An example of this type of hybrid framework stack would be an Ionic view layer backed by AngularJS, which is ultimately converted and rendered in a web view on the desired platform using PhoneGap and Cordova. Obviously, this type of WODE framework comes at the cost of added complexity.

Furthermore, the use of JavaScript brings with it its own design-based limitations and language-based issues. The goal of this article is not to debate the merits or flaws of any one language; however, as a project manager, the choice to use JavaScript in one’s mobile technical stack should not be made lightly. The following are just a few examples of well-written articles on why JavaScript should be avoided if possible:

Pros

Minimal development team. Hybrid frameworks enable a small development team—and particularly one whose primary knowledge base is web development—to quickly produce simple applications across multiple platforms. This allows a project manager to keep his team small as well as remove the need for his team to learn the native languages and frameworks for multiple platforms.

Faster development cycle. In addition to a smaller team, hybrid frameworks afford a faster development cycle when deploying to multiple platforms since only a single view layer needs to be designed using web technologies.

Cons

Potentially poor UX. The downside of only having to write a single view layer is that one is left with a single view layer. This can result in a poor UX since a one-size-fits-all approach to UI design fails to give one’s application a look and feel that is comfortable and familiar to users on all platforms. In addition, since hybrid applications are essentially a webview embedded within the UI, it can give users the impression that they are actually viewing a web page instead of interacting with a native application. This experience almost always has a negative impact upon user satisfaction, and ultimately retention.

Costly to customize. Improving upon the UX by designing customized UIs for each platform results in complex and unique UI frameworks that can be costly to create and difficult to maintain over time. Furthermore, in order to create UI elements that will help make one’s application stand out (e.g., animation, custom views, etc.), customized bridge components must be created to translate the high-level UI design into something that the lower-level framework, such as Cordova, will understand. In general, the more one customizes and improves the UX of a hybrid application, the more it diminishes the benefit of a fast and inexpensive design cycle.

Lower performance. Since hybrid applications render the application’s views within a webview, there is a large potential to make implementation mistakes when dealing with OS frameworks (e.g., networking, Bluetooth, on-device contacts, etc.), which result in vastly degraded performance. It is also worth noting that, even if great care is taken with regard to performance since everything is displayed via a webview, the hybrid applications’ maximum performance will always be slightly less than their native counterparts.

Non-trivial plugin management. Remember those custom features the design team spent weeks polishing, which was followed by another few weeks while the development team created the necessary bridge components so that Cordova could work with them? Well, they won’t work unless there is a supporting Cordova plugin for what the team is trying to achieve. This means one of two things: either the team creates it themselves or a suitable third-party plugin will need to be found that does the job. Unfortunately, more often than not, option two does not exist. As a result, it requires additional development time to create the custom plugins, followed by build support effort—over time—to manage the growing library of Cordova plugins required by the application. Of course, when Cordova updates occur, there is a high probability that these plugins will need to be updated as well.

OS support lag. The previously mentioned cascading bridge component/Cordova plugin issue is further exacerbated when the OS changes core functionality. Once Cordova, PhoneGap, and Ionic have been updated to support the changes, it is possible that the custom plugins and bridge components will need to be updated as well. Regardless of the order of magnitude this work would require, it results in additional time during which the application doesn’t support end users who have updated to the new OS. This, of course, is a worst-case scenario in which breaking, non-backward compatible changes are made by Apple or Google, which never happens… right? In general, any intermediate framework that is out of the developer’s control and that must be updated first only serves to delay the process. Finally, relying upon an intermediate framework can be a headache for project managers to plan around since the timing of these frameworks is such an unknown.

Non-hybrid Applications

Non-hybrid applications start out life just like their hybrid counterparts - a UI layer designed in a non-native platform language: React Native uses HTML/CSS backed by JavaScript or Xamarin, which is based on C# due to its .NET roots.

However, that is where the similarity ends. Non-hybrid applications compile down to native code and render the application using platform-native components instead of rendering via a webview. This results in a WODE framework that, at least on the surface, has the best of both worlds.

As previously discussed, choosing to use JavaScript should not be a decision that is made lightly, and it might be a deal-breaker for a development team that does not wish to learn or has no interest in using JavaScript.

Pros

Higher performance than hybrids. As one might expect, non-hybrids inherently have a higher performance than hybrid applications due to their ability to render the application using native UI components (buttons, views, layout managers, etc.) instead of relying on an embedded webview. Of course, developers are still free to write an application that performs either remarkably or horribly. The benefit of non-hybrid applications is simply that they have a higher performance baseline when compared to similar hybrid applications.

Minimal development team. Similar to hybrid frameworks, non-hybrids enable a small development team—and particularly one whose primary knowledge base is web development—to quickly produce simple applications across multiple platforms. This allows project managers to keep their team small and to preclude the team from learning the native languages and frameworks for multiple platforms.

Faster development cycle. In addition to a smaller team, non-hybrid frameworks afford a faster development cycle when deploying to multiple platforms, since only a single view layer needs to be designed.

Faster iterations (React). The React framework provides a powerful feature that allows changes to the application to be rendered in real time: no need to recompile, rebuild, etc. As a result, the React emulator is an incredibly powerful development tool that dramatically reduces the duration of each implementation cycle.

Cons

Costly to customize. Much like its hybrid counterpart, when non-hybrid applications require the UX to be improved by designing customized UIs for each platform, it results in complex and unique UI components that can be costly to create and difficult to maintain over time. This also means writing customized bridge components to supplement gaps in the framework’s native element support. Like hybrids, this cost diminishes the benefit of a fast and inexpensive design cycle, but unlike hybrid applications, bridge components are written for each desired platform in their native language. This means that instead of non-hybrid applications being a flexible alternative to a team that is primarily comprised of web developers, teams choosing the non-hybrid path have to learn not only the particular language of the framework (e.g., JSX or C#) but also the native language of each platform(Java, Obj-C, or Swift).

Third-party dependencies. This limitation takes two different forms. In the case of React Native, it takes the form of numerous dependencies, i.e., roughly 650. The result is that there is a very good chance at any particular time that one or more of those dependencies is out of date. It also means that in the event of a large OS-level change, there is a high probability that most or all of those dependencies will need to be updated. The potential saving grace is that Facebook uses React, so one will have the 300lb gorilla in their corner.

In the case of Xamarin, the third-party dependency issue is simply that it is extremely difficult to integrate them in the first place. Xamarin is aware of this issue and provides a utility tool called Sharpie. The tool’s purpose is to help with some of the integration, but unfortunately, Sharpie often attempts to compile and link incorrect resources, which forces the developer to undertake the painstakingly time-consuming task of manually modifying low-level compilation parameters in order to successfully complete the integration.

OS support lag. Non-hybrid applications are plagued by the same issue as hybrid ones. Any intermediate framework that is out of the developer’s control and that must be updated first only serves to delay the process of updating one’s application to support cutting-edge users. Furthermore, as stated before, relying upon an intermediate framework can be a headache for project managers to plan around since the timing of these frameworks is such an unknown.

Long-term support (React Native). This issue is specific to React Native and pertains to the odd fact that, to date, Facebook has not committed to a long-term support plan for its framework. It can be said that this is a low risk since the company utilizes its own framework for its mobile apps, but it is worth a pause for any project manager to consider why Facebook has refused to comment on the subject.

Choosing the Right Approach

When cost is not a primary consideration, Figure 2 shows that writing native applications is almost always the best choice when leveraging the makeup of the development team against the application’s requirements. When there are fewer development engineers than the number of desired platforms, it gets a bit more interesting. In that case, using React is the correct choice if the team is under a very tight release schedule; otherwise, going native is still the best option.

 

Team makeup

Figure 2: Comparison of team makeup vs. application requirements when choosing a mobile development path.

 

When the team is primarily a web development team, and a customized UX is required, it is better to have some team members change hats or add some team members in order to make one’s applications native. There really isn’t a feasible, maintainable framework option if an application requires custom elements, which many applications do.

However, if a custom UX is not required, then, depending on the release schedule, it might be better to go with Ionic or React. If one’s team has no time to learn JSX, then Ionic is the right choice. Otherwise, it is better to choose React since it already requires many third-party dependencies, and adding more isn’t going to impact one’s development cycle.

 

Cost vs. UX

Figure 3: Cost vs. User Experience when choosing a mobile development path.

 

Once the cost of the project is a primary concern, typically, the existing team makeup becomes less of a priority since the first step would be to put the proper team in place to execute the project plan for the projected cost. As shown in Figure 3, native applications have a higher starting cost than their WODE counterparts, but also a higher potential UX. Moreover, WODE applications will always be limited in their UX, regardless of how much money and resources are applied to the project.

I hope this article has shed some light on the pros and cons of various mobile development paths, as well as aided in weighing team makeup against both application requirements and the cost of the project. Its message was not to convey that WODE frameworks are inferior and should never be sought out, but rather that even though there are valid use cases for not going native, one should fully understand the ramifications of doing so.

Original article source at: https://www.toptal.com/

#go #native #mobile 

 How to Write Once, Deploy Everywhere: When to Go Native?
Hermann  Frami

Hermann Frami

1668041460

Spring Native Provides Beta Support for Compiling Spring Applications

Spring Native provides beta support for compiling Spring applications to native executables using GraalVM native-image compiler, in order to provide a native deployment option typically designed to be packaged in lightweight containers.

Watch the video and read the blog post of Spring Native Beta announcement to learn more. Announcing Spring Native Beta!

Quick start

The easiest way to start with Spring Native is probably to go to start.spring.io, add the Spring Native dependency, and read the reference documentation. Make sure to configure properly the Spring AOT Maven and Gradle plugins that are mandatory to get proper native support for your Spring application.

Play with the samples

NoteYou need to install the GraalVM native-image compiler, check the documentation for more details.

Download the latest release of this repository.

Go into the samples folder and pick one (e.g. cd samples/commandlinerunner)

Run ./build.sh which will run the regular JVM build, then a native image compilation, then test the result.

For more details on the samples see the samples documentation.

Contributing

If you have not previously done so, please sign the Contributor License Agreement. You will be reminded automatically when you submit the pull request.

Contributions are welcome, especially for adding support via pull requests for libraries widely used in the Spring ecosystem not yet support. Please refer to the how to contribute section for more details.

This project requires Java 11.

Spring Native will be replaced late November 2022 by Spring Boot 3 official native support, see this blog post for more details.

Download Details:

Author: Spring-projects-experimental
Source Code: https://github.com/spring-projects-experimental/spring-native 
License: Apache-2.0 license

#serverless #spring #native #springboot 

Spring Native Provides Beta Support for Compiling Spring Applications

Write Perl Bindings to Non-Perl Libraries with FFI

FFI::Platypus

Write Perl bindings to non-Perl libraries with FFI. No XS required.

SYNOPSIS

use FFI::Platypus 2.00;

# for all new code you should use api => 2
my $ffi = FFI::Platypus->new( api => 2 );
$ffi->lib(undef); # search libc

# call dynamically
$ffi->function( puts => ['string'] => 'int' )->call("hello world");

# attach as a xsub and call (much faster)
$ffi->attach( puts => ['string'] => 'int' );
puts("hello world");

DESCRIPTION

Platypus is a library for creating interfaces to machine code libraries written in languages like C, C++, Go, Fortran, Rust, Pascal. Essentially anything that gets compiled into machine code. This implementation uses libffi to accomplish this task. libffi is battle tested by a number of other scripting and virtual machine languages, such as Python and Ruby to serve a similar role. There are a number of reasons why you might want to write an extension with Platypus instead of XS:

FFI / Platypus does not require messing with the guts of Perl

XS is less of an API and more of the guts of perl splayed out to do whatever you want. That may at times be very powerful, but it can also be a frustrating exercise in hair pulling.

FFI / Platypus is portable

Lots of languages have FFI interfaces, and it is subjectively easier to port an extension written in FFI in Perl or another language to FFI in another language or Perl. One goal of the Platypus Project is to reduce common interface specifications to a common format like JSON that could be shared between different languages.

FFI / Platypus could be a bridge to Raku

One of those "other" languages could be Raku and Raku already has an FFI interface I am told.

FFI / Platypus can be reimplemented

In a bright future with multiple implementations of Perl 5, each interpreter will have its own implementation of Platypus, allowing extensions to be written once and used on multiple platforms, in much the same way that Ruby-FFI extensions can be use in Ruby, JRuby and Rubinius.

FFI / Platypus is pure perl (sorta)

One Platypus script or module works on any platform where the libraries it uses are available. That means you can deploy your Platypus script in a shared filesystem where they may be run on different platforms. It also means that Platypus modules do not need to be installed in the platform specific Perl library path.

FFI / Platypus is not C or C++ centric

XS is implemented primarily as a bunch of C macros, which requires at least some understanding of C, the C pre-processor, and some C++ caveats (since on some platforms Perl is compiled and linked with a C++ compiler). Platypus on the other hand could be used to call other compiled languages, like Fortran, Go, Rust, Pascal, C++, or even assembly, allowing you to focus on your strengths.

FFI / Platypus does not require a parser

Inline isolates the extension developer from XS to some extent, but it also requires a parser. The various Inline language bindings are a great technical achievement, but I think writing a parser for every language that you want to interface with is a bit of an anti-pattern.

This document consists of an API reference, a set of examples, some support and development (for contributors) information. If you are new to Platypus or FFI, you may want to skip down to the EXAMPLES to get a taste of what you can do with Platypus.

Platypus has extensive documentation of types at FFI::Platypus::Type and its custom types API at FFI::Platypus::API.

You are strongly encouraged to use API level 1 for all new code. There are a number of improvements and design fixes that you get for free. You should even consider updating existing modules to use API level 1 where feasible. How do I do that you might ask? Simply pass in the API level to the platypus constructor.

my $ffi = FFI::Platypus->new( api => 2 );

The Platypus documentation has already been updated to assume API level 1.

CONSTRUCTORS

new

my $ffi = FFI::Platypus->new( api => 2, %options);

Create a new instance of FFI::Platypus.

Any types defined with this instance will be valid for this instance only, so you do not need to worry about stepping on the toes of other CPAN FFI / Platypus Authors.

Any functions found will be out of the list of libraries specified with the lib attribute.

options

api

[version 0.91]

Sets the API level. Legal values are

0

Original API level. See FFI::Platypus::TypeParser::Version0 for details on the differences.

1

Enable version 1 API type parser which allows pass-by-value records and type decoration on basic types.

2

Enable version 2 API. All new code should be written with this set to 1! The Platypus documentation assumes this api level is set.

API version 2 is identical to version 1, except:

Pointer functions that return NULL will return undef instead of empty list

This fixes a long standing design bug in Platypus.

Array references may be passed to pointer argument types

This replicates the behavior of array argument types with no size. So the types sint8* and sint8[] behave identically when an array reference is passed in. They differ in that, as before, you can pass a scalar reference into type sint8*.

The fixed string type can be specified without pointer modifier

That is you can use string(10) instead of string(10)* as you were previously able to in API 0.

lib

Either a pathname (string) or a list of pathnames (array ref of strings) to pre-populate the lib attribute. Use [undef] to search the current process for symbols.

0.48

undef (without the array reference) can be used to search the current process for symbols.

ignore_not_found

[version 0.15]

Set the ignore_not_found attribute.

lang

[version 0.18]

Set the lang attribute.

ATTRIBUTES

lib

$ffi->lib($path1, $path2, ...);
my @paths = $ffi->lib;

The list of libraries to search for symbols in.

The most portable and reliable way to find dynamic libraries is by using FFI::CheckLib, like this:

use FFI::CheckLib 0.06;
$ffi->lib(find_lib_or_die lib => 'archive');
  # finds libarchive.so on Linux
  #       libarchive.bundle on OS X
  #       libarchive.dll (or archive.dll) on Windows
  #       cygarchive-13.dll on Cygwin
  #       ...
  # and will die if it isn't found

FFI::CheckLib has a number of options, such as checking for specific symbols, etc. You should consult the documentation for that module.

As a special case, if you add undef as a "library" to be searched, Platypus will also search the current process for symbols. This is mostly useful for finding functions in the standard C library, without having to know the name of the standard c library for your platform (as it turns out it is different just about everywhere!).

You may also use the "find_lib" method as a shortcut:

$ffi->find_lib( lib => 'archive' );

ignore_not_found

[version 0.15]

$ffi->ignore_not_found(1);
my $ignore_not_found = $ffi->ignore_not_found;

Normally the attach and function methods will throw an exception if it cannot find the name of the function you provide it. This will change the behavior such that function will return undef when the function is not found and attach will ignore functions that are not found. This is useful when you are writing bindings to a library and have many optional functions and you do not wish to wrap every call to function or attach in an eval.

lang

[version 0.18]

$ffi->lang($language);

Specifies the foreign language that you will be interfacing with. The default is C. The foreign language specified with this attribute changes the default native types (for example, if you specify Rust, you will get i32 as an alias for sint32 instead of int as you do with C).

If the foreign language plugin supports it, this will also enable Platypus to find symbols using the demangled names (for example, if you specify CPP for C++ you can use method names like Foo::get_bar() with "attach" or "function".

api

[version 1.11]

my $level = $ffi->api;

Returns the API level of the Platypus instance.

METHODS

type

$ffi->type($typename);
$ffi->type($typename => $alias);

Define a type. The first argument is the native or C name of the type. The second argument (optional) is an alias name that you can use to refer to this new type. See FFI::Platypus::Type for legal type definitions.

Examples:

$ffi->type('sint32');            # only checks to see that sint32 is a valid type
$ffi->type('sint32' => 'myint'); # creates an alias myint for sint32
$ffi->type('bogus');             # dies with appropriate diagnostic

custom_type

$ffi->custom_type($alias => {
  native_type         => $native_type,
  native_to_perl      => $coderef,
  perl_to_native      => $coderef,
  perl_to_native_post => $coderef,
});

Define a custom type. See FFI::Platypus::Type#Custom-Types for details.

load_custom_type

$ffi->load_custom_type($name => $alias, @type_args);

Load the custom type defined in the module $name, and make an alias $alias. If the custom type requires any arguments, they may be passed in as @type_args. See FFI::Platypus::Type#Custom-Types for details.

If $name contains :: then it will be assumed to be a fully qualified package name. If not, then FFI::Platypus::Type:: will be prepended to it.

types

my @types = $ffi->types;
my @types = FFI::Platypus->types;

Returns the list of types that FFI knows about. This will include the native libffi types (example: sint32, opaque and double) and the normal C types (example: unsigned int, uint32_t), any types that you have defined using the type method, and custom types.

The list of types that Platypus knows about varies somewhat from platform to platform, FFI::Platypus::Type includes a list of the core types that you can always count on having access to.

It can also be called as a class method, in which case, no user defined or custom types will be included in the list.

type_meta

my $meta = $ffi->type_meta($type_name);
my $meta = FFI::Platypus->type_meta($type_name);

Returns a hash reference with the meta information for the given type.

It can also be called as a class method, in which case, you won't be able to get meta data on user defined types.

The format of the meta data is implementation dependent and subject to change. It may be useful for display or debugging.

Examples:

my $meta = $ffi->type_meta('int');        # standard int type
my $meta = $ffi->type_meta('int[64]');    # array of 64 ints
$ffi->type('int[128]' => 'myintarray');
my $meta = $ffi->type_meta('myintarray'); # array of 128 ints

mangler

$ffi->mangler(\&mangler);

Specify a customer mangler to be used for symbol lookup. This is usually useful when you are writing bindings for a library where all of the functions have the same prefix. Example:

$ffi->mangler(sub {
  my($symbol) = @_;
  return "foo_$symbol";
});

$ffi->function( get_bar => [] => 'int' );  # attaches foo_get_bar

my $f = $ffi->function( set_baz => ['int'] => 'void' );
$f->call(22); # calls foo_set_baz

function

my $function = $ffi->function($name => \@argument_types => $return_type);
my $function = $ffi->function($address => \@argument_types => $return_type);
my $function = $ffi->function($name => \@argument_types => $return_type, \&wrapper);
my $function = $ffi->function($address => \@argument_types => $return_type, \&wrapper);

Returns an object that is similar to a code reference in that it can be called like one.

Caveat: many situations require a real code reference, so at the price of a performance penalty you can get one like this:

my $function = $ffi->function(...);
my $coderef = sub { $function->(@_) };

It may be better, and faster to create a real Perl function using the attach method.

In addition to looking up a function by name you can provide the address of the symbol yourself:

my $address = $ffi->find_symbol('my_function');
my $function = $ffi->function($address => ...);

Under the covers, function uses find_symbol when you provide it with a name, but it is useful to keep this in mind as there are alternative ways of obtaining a functions address. Example: a C function could return the address of another C function that you might want to call, or modules such as FFI::TinyCC produce machine code at runtime that you can call from Platypus.

[version 0.76]

If the last argument is a code reference, then it will be used as a wrapper around the function when called. The first argument to the wrapper will be the inner function, or if it is later attached an xsub. This can be used if you need to verify/modify input/output data.

Examples:

my $function = $ffi->function('my_function_name', ['int', 'string'] => 'string');
my $return_string = $function->(1, "hi there");

[version 0.91]

my $function = $ffi->function( $name => \@fixed_argument_types => \@var_argument_types => $return_type);
my $function = $ffi->function( $name => \@fixed_argument_types => \@var_argument_types => $return_type, \&wrapper);
my $function = $ffi->function( $name => \@fixed_argument_types => \@var_argument_types);
my $function = $ffi->function( $name => \@fixed_argument_types => \@var_argument_types => \&wrapper);

Version 0.91 and later allows you to creat functions for c variadic functions (such as printf, scanf, etc) which can take a variable number of arguments. The first set of arguments are the fixed set, the second set are the variable arguments to bind with. The variable argument types must be specified in order to create a function object, so if you need to call variadic function with different set of arguments then you will need to create a new function object each time:

# int printf(const char *fmt, ...);
$ffi->function( printf => ['string'] => ['int'] => 'int' )
    ->call("print integer %d\n", 42);
$ffi->function( printf => ['string'] => ['string'] => 'int' )
    ->call("print string %s\n", 'platypus');

Some older versions of libffi and possibly some platforms may not support variadic functions. If you try to create a one, then an exception will be thrown.

[version 1.26]

If the return type is omitted then void will be the assumed return type.

attach

$ffi->attach($name => \@argument_types => $return_type);
$ffi->attach([$c_name => $perl_name] => \@argument_types => $return_type);
$ffi->attach([$address => $perl_name] => \@argument_types => $return_type);
$ffi->attach($name => \@argument_types => $return_type, \&wrapper);
$ffi->attach([$c_name => $perl_name] => \@argument_types => $return_type, \&wrapper);
$ffi->attach([$address => $perl_name] => \@argument_types => $return_type, \&wrapper);

Find and attach a C function as a real live Perl xsub. The advantage of attaching a function over using the function method is that it is much much much faster since no object resolution needs to be done. The disadvantage is that it locks the function and the FFI::Platypus instance into memory permanently, since there is no way to deallocate an xsub.

If just one $name is given, then the function will be attached in Perl with the same name as it has in C. The second form allows you to give the Perl function a different name. You can also provide an address (the third form), just like with the function method.

Examples:

$ffi->attach('my_function_name', ['int', 'string'] => 'string');
$ffi->attach(['my_c_function_name' => 'my_perl_function_name'], ['int', 'string'] => 'string');
my $string1 = my_function_name($int);
my $string2 = my_perl_function_name($int);

[version 0.20]

If the last argument is a code reference, then it will be used as a wrapper around the attached xsub. The first argument to the wrapper will be the inner xsub. This can be used if you need to verify/modify input/output data.

Examples:

$ffi->attach('my_function', ['int', 'string'] => 'string', sub {
  my($my_function_xsub, $integer, $string) = @_;
  $integer++;
  $string .= " and another thing";
  my $return_string = $my_function_xsub->($integer, $string);
  $return_string =~ s/Belgium//; # HHGG remove profanity
  $return_string;
});

[version 0.91]

$ffi->attach($name => \@fixed_argument_types => \@var_argument_types, $return_type);
$ffi->attach($name => \@fixed_argument_types => \@var_argument_types, $return_type, \&wrapper);

As of version 0.91 you can attach a variadic functions, if it is supported by the platform / libffi that you are using. For details see the function documentation. If not supported by the implementation then an exception will be thrown.

closure

my $closure = $ffi->closure($coderef);
my $closure = FFI::Platypus->closure($coderef);

Prepares a code reference so that it can be used as a FFI closure (a Perl subroutine that can be called from C code). For details on closures, see FFI::Platypus::Type#Closures and FFI::Platypus::Closure.

cast

my $converted_value = $ffi->cast($original_type, $converted_type, $original_value);

The cast function converts an existing $original_value of type $original_type into one of type $converted_type. Not all types are supported, so care must be taken. For example, to get the address of a string, you can do this:

my $address = $ffi->cast('string' => 'opaque', $string_value);

Something that won't work is trying to cast an array to anything:

my $address = $ffi->cast('int[10]' => 'opaque', \@list);  # WRONG

attach_cast

$ffi->attach_cast("cast_name", $original_type, $converted_type);
$ffi->attach_cast("cast_name", $original_type, $converted_type, \&wrapper);
my $converted_value = cast_name($original_value);

This function attaches a cast as a permanent xsub. This will make it faster and may be useful if you are calling a particular cast a lot.

[version 1.26]

A wrapper may be added as the last argument to attach_cast and works just like the wrapper for attach and function methods.

sizeof

my $size = $ffi->sizeof($type);
my $size = FFI::Platypus->sizeof($type);

Returns the total size of the given type in bytes. For example to get the size of an integer:

my $intsize = $ffi->sizeof('int');   # usually 4
my $longsize = $ffi->sizeof('long'); # usually 4 or 8 depending on platform

You can also get the size of arrays

my $intarraysize = $ffi->sizeof('int[64]');  # usually 4*64
my $intarraysize = $ffi->sizeof('long[64]'); # usually 4*64 or 8*64
                                             # depending on platform

Keep in mind that "pointer" types will always be the pointer / word size for the platform that you are using. This includes strings, opaque and pointers to other types.

This function is not very fast, so you might want to save this value as a constant, particularly if you need the size in a loop with many iterations.

alignof

[version 0.21]

my $align = $ffi->alignof($type);

Returns the alignment of the given type in bytes.

kindof

[version 1.24]

my $kind = $ffi->kindof($type);

Returns the kind of a type. This is a string with a value of one of

  • void
  • scalar
  • string
  • closure
  • record
  • record-value
  • pointer
  • array
  • object

countof

[version 1.24]

my $count = $ffi->countof($type);

For array types returns the number of elements in the array (returns 0 for variable length array). For the void type returns 0. Returns 1 for all other types.

def

[version 1.24]

$ffi->def($package, $type, $value);
my $value = $ff->def($package, $type);

This method allows you to store data for types. If the $package is not provided, then the caller's package will be used. $type must be a legal Platypus type for the FFI::Platypus instance.

unitof

[version 1.24]

my $unittype = $ffi->unitof($type);

For array and pointer types, returns the basic type without the array or pointer part. In other words, for sin16[] or sint16* it will return sint16.

find_lib

[version 0.20]

$ffi->find_lib( lib => $libname );

This is just a shortcut for calling FFI::CheckLib#find_lib and updating the "lib" attribute appropriately. Care should be taken though, as this method simply passes its arguments to FFI::CheckLib#find_lib, so if your module or script is depending on a specific feature in FFI::CheckLib then make sure that you update your prerequisites appropriately.

find_symbol

my $address = $ffi->find_symbol($name);

Return the address of the given symbol (usually function).

bundle

[version 0.96 api = 1+]

$ffi->bundle($package, \@args);
$ffi->bundle(\@args);
$ffi->bundle($package);
$ffi->bundle;

This is an interface for bundling compiled code with your distribution intended to eventually replace the package method documented above. See FFI::Platypus::Bundle for details on how this works.

package

[version 0.15 api = 0]

$ffi->package($package, $file); # usually __PACKAGE__ and __FILE__ can be used
$ffi->package;                  # autodetect

Note: This method is officially discouraged in favor of bundle described above.

If you use FFI::Build (or the older deprecated Module::Build::FFI to bundle C code with your distribution, you can use this method to tell the FFI::Platypus instance to look for symbols that came with the dynamic library that was built when your distribution was installed.

abis

my $href = $ffi->abis;
my $href = FFI::Platypus->abis;

Get the legal ABIs supported by your platform and underlying implementation. What is supported can vary a lot by CPU and by platform, or even between 32 and 64 bit on the same CPU and platform. They keys are the "ABI" names, also known as "calling conventions". The values are integers used internally by the implementation to represent those ABIs.

abi

$ffi->abi($name);

Set the ABI or calling convention for use in subsequent calls to "function" or "attach". May be either a string name or integer value from the "abis" method above.

EXAMPLES

Here are some examples. These examples are provided in full with the Platypus distribution in the "examples" directory. There are also some more examples in FFI::Platypus::Type that are related to types.

Integer conversions

use FFI::Platypus 2.00;

my $ffi = FFI::Platypus->new( api => 2 );
$ffi->lib(undef);

$ffi->attach(puts => ['string'] => 'int');
$ffi->attach(atoi => ['string'] => 'int');

puts(atoi('56'));

Discussion: puts and atoi should be part of the standard C library on all platforms. puts prints a string to standard output, and atoi converts a string to integer. Specifying undef as a library tells Platypus to search the current process for symbols, which includes the standard c library.

libnotify

use FFI::CheckLib;
use FFI::Platypus 2.00;

# NOTE: I ported this from anoter Perl FFI library and it seems to work most
# of the time, but also seems to SIGSEGV sometimes.  I saw the same behavior
# in the old version, and am not really familiar with the libnotify API to
# say what is the cause.  Patches welcome to fix it.

my $ffi = FFI::Platypus->new( api => 2 );
$ffi->lib(find_lib_or_exit lib => 'notify');

$ffi->attach(notify_init   => ['string'] => 'void');
$ffi->attach(notify_uninit => []       => 'void');
$ffi->attach([notify_notification_new    => 'notify_new']    => ['string', 'string', 'string']           => 'opaque');
$ffi->attach([notify_notification_update => 'notify_update'] => ['opaque', 'string', 'string', 'string'] => 'void');
$ffi->attach([notify_notification_show   => 'notify_show']   => ['opaque', 'opaque']                     => 'void');

notify_init('FFI::Platypus');
my $n = notify_new('','','');
notify_update($n, 'FFI::Platypus', 'It works!!!', 'media-playback-start');
notify_show($n, undef);
notify_uninit();

Discussion: libnotify is a desktop GUI notification library for the GNOME Desktop environment. This script sends a notification event that should show up as a balloon, for me it did so in the upper right hand corner of my screen.

The most portable way to find the correct name and location of a dynamic library is via the FFI::CheckLib#find_lib family of functions. If you are putting together a CPAN distribution, you should also consider using FFI::CheckLib#check_lib_or_exit function in your Build.PL or Makefile.PL file (If you are using Dist::Zilla, check out the Dist::Zilla::Plugin::FFI::CheckLib plugin). This will provide a user friendly diagnostic letting the user know that the required library is missing, and reduce the number of bogus CPAN testers results that you will get.

Also in this example, we rename some of the functions when they are placed into Perl space to save typing:

$ffi->attach( [notify_notification_new => 'notify_new']
  => ['string','string','string']
  => 'opaque'
);

When you specify a list reference as the "name" of the function the first element is the symbol name as understood by the dynamic library. The second element is the name as it will be placed in Perl space.

Later, when we call notify_new:

my $n = notify_new('','','');

We are really calling the C function notify_notification_new.

Allocating and freeing memory

use FFI::Platypus 2.00;
use FFI::Platypus::Memory qw( malloc free memcpy );

my $ffi = FFI::Platypus->new( api => 2 );
my $buffer = malloc 12;

memcpy $buffer, $ffi->cast('string' => 'opaque', "hello there"), length "hello there\0";

print $ffi->cast('opaque' => 'string', $buffer), "\n";

free $buffer;

Discussion: malloc and free are standard memory allocation functions available from the standard c library and. Interfaces to these and other memory related functions are provided by the FFI::Platypus::Memory module.

structured data records

use FFI::Platypus 2.00;
use FFI::C;

my $ffi = FFI::Platypus->new(
  api => 2,
  lib => [undef],
);
FFI::C->ffi($ffi);

package Unix::TimeStruct {

  FFI::C->struct(tm => [
    tm_sec    => 'int',
    tm_min    => 'int',
    tm_hour   => 'int',
    tm_mday   => 'int',
    tm_mon    => 'int',
    tm_year   => 'int',
    tm_wday   => 'int',
    tm_yday   => 'int',
    tm_isdst  => 'int',
    tm_gmtoff => 'long',
    _tm_zone  => 'opaque',
  ]);

  # For now 'string' is unsupported by FFI::C, but we
  # can cast the time zone from an opaque pointer to
  # string.
  sub tm_zone {
    my $self = shift;
    $ffi->cast('opaque', 'string', $self->_tm_zone);
  }

  # attach the C localtime function
  $ffi->attach( localtime => ['time_t*'] => 'tm', sub {
    my($inner, $class, $time) = @_;
    $time = time unless defined $time;
    $inner->(\$time);
  });
}

# now we can actually use our Unix::TimeStruct class
my $time = Unix::TimeStruct->localtime;
printf "time is %d:%d:%d %s\n",
  $time->tm_hour,
  $time->tm_min,
  $time->tm_sec,
  $time->tm_zone;

Discussion: C and other machine code languages frequently provide interfaces that include structured data records (known as "structs" in C). They sometimes provide an API in which you are expected to manipulate these records before and/or after passing them along to C functions. For C pointers to structs, unions and arrays of structs and unions, the easiest interface to use is via FFI::C. If you are working with structs that must be passed as values (not pointers), then you want to use the FFI::Platypus::Record class instead. We will discuss this class later.

The C localtime function takes a pointer to a C struct. We simply define the members of the struct using the FFI::C struct method. Because we used the ffi method to tell FFI::C to use our local instance of FFI::Platypus it registers the tm type for us, and we can just start using it as a return type!

structured data records by-value

libuuid

use FFI::CheckLib;
use FFI::Platypus 2.00;
use FFI::Platypus::Memory qw( malloc free );

my $ffi = FFI::Platypus->new( api => 2 );
$ffi->lib(find_lib_or_exit lib => 'uuid');
$ffi->type('string(37)*' => 'uuid_string');
$ffi->type('record(16)*' => 'uuid_t');

$ffi->attach(uuid_generate => ['uuid_t'] => 'void');
$ffi->attach(uuid_unparse  => ['uuid_t','uuid_string'] => 'void');

my $uuid = "\0" x $ffi->sizeof('uuid_t');
uuid_generate($uuid);

my $string = "\0" x $ffi->sizeof('uuid_string');
uuid_unparse($uuid, $string);

print "$string\n";

Discussion: libuuid is a library used to generate unique identifiers (UUID) for objects that may be accessible beyond the local system. The library is or was part of the Linux e2fsprogs package.

Knowing the size of objects is sometimes important. In this example, we use the sizeof function to get the size of 16 characters (in this case it is simply 16 bytes). We also know that the strings "deparsed" by uuid_unparse are exactly 37 bytes.

puts and getpid

use FFI::Platypus 2.00;

my $ffi = FFI::Platypus->new( api => 2 );
$ffi->lib(undef);

$ffi->attach(puts => ['string'] => 'int');
$ffi->attach(getpid => [] => 'int');

puts(getpid());

Discussion: puts is part of standard C library on all platforms. getpid is available on Unix type platforms.

Math library

use FFI::Platypus 2.00;
use FFI::CheckLib;

my $ffi = FFI::Platypus->new( api => 2 );
$ffi->lib(undef);
$ffi->attach(puts => ['string'] => 'int');
$ffi->attach(fdim => ['double','double'] => 'double');

puts(fdim(7.0, 2.0));

$ffi->attach(cos => ['double'] => 'double');

puts(cos(2.0));

$ffi->attach(fmax => ['double', 'double'] => 'double');

puts(fmax(2.0,3.0));

Discussion: On UNIX the standard c library math functions are frequently provided in a separate library libm, so you could search for those symbols in "libm.so", but that won't work on non-UNIX platforms like Microsoft Windows. Fortunately Perl uses the math library so these symbols are already in the current process so you can use undef as the library to find them.

Strings

use FFI::Platypus 2.00;

my $ffi = FFI::Platypus->new( api => 2 );
$ffi->lib(undef);
$ffi->attach(puts => ['string'] => 'int');
$ffi->attach(strlen => ['string'] => 'int');

puts(strlen('somestring'));

$ffi->attach(strstr => ['string','string'] => 'string');

puts(strstr('somestring', 'string'));

#attach puts => [string] => int;

puts(puts("lol"));

$ffi->attach(strerror => ['int'] => 'string');

puts(strerror(2));

Discussion: ASCII and UTF-8 Strings are not a native type to libffi but the are handled seamlessly by Platypus. If you need to talk to an API that uses so called "wide" strings (APIs which use const wchar_t* or wchar_t*), then you will want to use the wide string type plugin FFI::Platypus::Type::WideString. APIs which use other arbitrary encodings can be accessed by converting your Perl strings manually with the Encode module.

Attach function from pointer

use FFI::TinyCC;
use FFI::Platypus 2.00;

my $ffi = FFI::Platypus->new( api => 2 );
my $tcc = FFI::TinyCC->new;

$tcc->compile_string(q{
  int
  add(int a, int b)
  {
    return a+b;
  }
});

my $address = $tcc->get_symbol('add');

$ffi->attach( [ $address => 'add' ] => ['int','int'] => 'int' );

print add(1,2), "\n";

Discussion: Sometimes you will have a pointer to a function from a source other than Platypus that you want to call. You can use that address instead of a function name for either of the function or attach methods. In this example we use FFI::TinyCC to compile a short piece of C code and to give us the address of one of its functions, which we then use to create a perl xsub to call it.

FFI::TinyCC embeds the Tiny C Compiler (tcc) to provide a just-in-time (JIT) compilation service for FFI.

libzmq

use constant ZMQ_IO_THREADS  => 1;
use constant ZMQ_MAX_SOCKETS => 2;
use constant ZMQ_REQ => 3;
use constant ZMQ_REP => 4;
use FFI::CheckLib qw( find_lib_or_exit );
use FFI::Platypus 2.00;
use FFI::Platypus::Memory qw( malloc );
use FFI::Platypus::Buffer qw( scalar_to_buffer buffer_to_scalar );

my $endpoint = "ipc://zmq-ffi-$$";
my $ffi = FFI::Platypus->new( api => 2 );

$ffi->lib(undef); # for puts
$ffi->attach(puts => ['string'] => 'int');

$ffi->lib(find_lib_or_exit lib => 'zmq');
$ffi->attach(zmq_version => ['int*', 'int*', 'int*'] => 'void');

my($major,$minor,$patch);
zmq_version(\$major, \$minor, \$patch);
puts("libzmq version $major.$minor.$patch");
die "this script only works with libzmq 3 or better" unless $major >= 3;

$ffi->type('opaque'       => 'zmq_context');
$ffi->type('opaque'       => 'zmq_socket');
$ffi->type('opaque'       => 'zmq_msg_t');
$ffi->attach(zmq_ctx_new  => [] => 'zmq_context');
$ffi->attach(zmq_ctx_set  => ['zmq_context', 'int', 'int'] => 'int');
$ffi->attach(zmq_socket   => ['zmq_context', 'int'] => 'zmq_socket');
$ffi->attach(zmq_connect  => ['opaque', 'string'] => 'int');
$ffi->attach(zmq_bind     => ['zmq_socket', 'string'] => 'int');
$ffi->attach(zmq_send     => ['zmq_socket', 'opaque', 'size_t', 'int'] => 'int');
$ffi->attach(zmq_msg_init => ['zmq_msg_t'] => 'int');
$ffi->attach(zmq_msg_recv => ['zmq_msg_t', 'zmq_socket', 'int'] => 'int');
$ffi->attach(zmq_msg_data => ['zmq_msg_t'] => 'opaque');
$ffi->attach(zmq_errno    => [] => 'int');
$ffi->attach(zmq_strerror => ['int'] => 'string');

my $context = zmq_ctx_new();
zmq_ctx_set($context, ZMQ_IO_THREADS, 1);

my $socket1 = zmq_socket($context, ZMQ_REQ);
zmq_connect($socket1, $endpoint);

my $socket2 = zmq_socket($context, ZMQ_REP);
zmq_bind($socket2, $endpoint);

do { # send
  our $sent_message = "hello there";
  my($pointer, $size) = scalar_to_buffer $sent_message;
  my $r = zmq_send($socket1, $pointer, $size, 0);
  die zmq_strerror(zmq_errno()) if $r == -1;
};

do { # recv
  my $msg_ptr  = malloc 100;
  zmq_msg_init($msg_ptr);
  my $size     = zmq_msg_recv($msg_ptr, $socket2, 0);
  die zmq_strerror(zmq_errno()) if $size == -1;
  my $data_ptr = zmq_msg_data($msg_ptr);
  my $recv_message = buffer_to_scalar $data_ptr, $size;
  print "recv_message = $recv_message\n";
};

Discussion: ØMQ is a high-performance asynchronous messaging library. There are a few things to note here.

Firstly, sometimes there may be multiple versions of a library in the wild and you may need to verify that the library on a system meets your needs (alternatively you could support multiple versions and configure your bindings dynamically). Here we use zmq_version to ask libzmq which version it is.

zmq_version returns the version number via three integer pointer arguments, so we use the pointer to integer type: int *. In order to pass pointer types, we pass a reference. In this case it is a reference to an undefined value, because zmq_version will write into the pointers the output values, but you can also pass in references to integers, floating point values and opaque pointer types. When the function returns the $major variable (and the others) has been updated and we can use it to verify that it supports the API that we require.

Notice that we define three aliases for the opaque type: zmq_context, zmq_socket and zmq_msg_t. While this isn't strictly necessary, since Platypus and C treat all three of these types the same, it is useful form of documentation that helps describe the functionality of the interface.

Finally we attach the necessary functions, send and receive a message. If you are interested, there is a fully fleshed out ØMQ Perl interface implemented using FFI called ZMQ::FFI.

libarchive

use FFI::Platypus 2.00;
use FFI::CheckLib qw( find_lib_or_exit );

# This example uses FreeBSD's libarchive to list the contents of any
# archive format that it suppors.  We've also filled out a part of
# the ArchiveWrite class that could be used for writing archive formats
# supported by libarchive

my $ffi = FFI::Platypus->new( api => 2 );
$ffi->lib(find_lib_or_exit lib => 'archive');
$ffi->type('object(Archive)'      => 'archive_t');
$ffi->type('object(ArchiveRead)'  => 'archive_read_t');
$ffi->type('object(ArchiveWrite)' => 'archive_write_t');
$ffi->type('object(ArchiveEntry)' => 'archive_entry_t');

package Archive;

# base class is "abstract" having no constructor or destructor

$ffi->mangler(sub {
  my($name) = @_;
  "archive_$name";
});
$ffi->attach( error_string => ['archive_t'] => 'string' );

package ArchiveRead;

our @ISA = qw( Archive );

$ffi->mangler(sub {
  my($name) = @_;
  "archive_read_$name";
});

$ffi->attach( new                   => ['string']                        => 'archive_read_t' );
$ffi->attach( [ free => 'DESTROY' ] => ['archive_t']                     => 'void' );
$ffi->attach( support_filter_all    => ['archive_t']                     => 'int' );
$ffi->attach( support_format_all    => ['archive_t']                     => 'int' );
$ffi->attach( open_filename         => ['archive_t','string','size_t']   => 'int' );
$ffi->attach( next_header2          => ['archive_t', 'archive_entry_t' ] => 'int' );
$ffi->attach( data_skip             => ['archive_t']                     => 'int' );
# ... define additional read methods

package ArchiveWrite;

our @ISA = qw( Archive );

$ffi->mangler(sub {
  my($name) = @_;
  "archive_write_$name";
});

$ffi->attach( new                   => ['string'] => 'archive_write_t' );
$ffi->attach( [ free => 'DESTROY' ] => ['archive_write_t'] => 'void' );
# ... define additional write methods

package ArchiveEntry;

$ffi->mangler(sub {
  my($name) = @_;
  "archive_entry_$name";
});

$ffi->attach( new => ['string']     => 'archive_entry_t' );
$ffi->attach( [ free => 'DESTROY' ] => ['archive_entry_t'] => 'void' );
$ffi->attach( pathname              => ['archive_entry_t'] => 'string' );
# ... define additional entry methods

package main;

use constant ARCHIVE_OK => 0;

# this is a Perl version of the C code here:
# https://github.com/libarchive/libarchive/wiki/Examples#List_contents_of_Archive_stored_in_File

my $archive_filename = shift @ARGV;
unless(defined $archive_filename)
{
  print "usage: $0 archive.tar\n";
  exit;
}

my $archive = ArchiveRead->new;
$archive->support_filter_all;
$archive->support_format_all;

my $r = $archive->open_filename($archive_filename, 1024);
die "error opening $archive_filename: ", $archive->error_string
  unless $r == ARCHIVE_OK;

my $entry = ArchiveEntry->new;

while($archive->next_header2($entry) == ARCHIVE_OK)
{
  print $entry->pathname, "\n";
  $archive->data_skip;
}

Discussion: libarchive is the implementation of tar for FreeBSD provided as a library and available on a number of platforms.

One interesting thing about libarchive is that it provides a kind of object oriented interface via opaque pointers. This example creates an abstract class Archive, and concrete classes ArchiveWrite, ArchiveRead and ArchiveEntry. The concrete classes can even be inherited from and extended just like any Perl classes because of the way the custom types are implemented. We use Platypus's object type for this implementation, which is a wrapper around an opaque (can also be an integer) type that is blessed into a particular class.

Another advanced feature of this example is that we define a mangler to modify the symbol resolution for each class. This means we can do this when we define a method for Archive:

$ffi->attach( support_filter_all => ['archive_t'] => 'int' );

Rather than this:

$ffi->attach(
  [ archive_read_support_filter_all => 'support_read_filter_all' ] =>
  ['archive_t'] => 'int' );
);

unix open

use FFI::Platypus 2.00;

{
  package FD;

  use constant O_RDONLY => 0;
  use constant O_WRONLY => 1;
  use constant O_RDWR   => 2;

  use constant IN  => bless \do { my $in=0  }, __PACKAGE__;
  use constant OUT => bless \do { my $out=1 }, __PACKAGE__;
  use constant ERR => bless \do { my $err=2 }, __PACKAGE__;

  my $ffi = FFI::Platypus->new( api => 2, lib => [undef]);

  $ffi->type('object(FD,int)' => 'fd');

  $ffi->attach( [ 'open' => 'new' ] => [ 'string', 'int', 'mode_t' ] => 'fd' => sub {
    my($xsub, $class, $fn, @rest) = @_;
    my $fd = $xsub->($fn, @rest);
    die "error opening $fn $!" if $$fd == -1;
    $fd;
  });

  $ffi->attach( write => ['fd', 'string', 'size_t' ] => 'ssize_t' );
  $ffi->attach( read  => ['fd', 'string', 'size_t' ] => 'ssize_t' );
  $ffi->attach( close => ['fd'] => 'int' );
}

my $fd = FD->new("$0", FD::O_RDONLY);

my $buffer = "\0" x 10;

while(my $br = $fd->read($buffer, 10))
{
  FD::OUT->write($buffer, $br);
}

$fd->close;

Discussion: The Unix file system calls use an integer handle for each open file. We can use the same object type that we used for libarchive above, except we let platypus know that the underlying type is int instead of opaque (the latter being the default for the object type). Mainly just for demonstration since Perl has much better IO libraries, but now we have an OO interface to the Unix IO functions.

bzip2

use FFI::Platypus 2.00;
use FFI::CheckLib qw( find_lib_or_die );
use FFI::Platypus::Buffer qw( scalar_to_buffer buffer_to_scalar );
use FFI::Platypus::Memory qw( malloc free );

my $ffi = FFI::Platypus->new( api => 2 );
$ffi->lib(find_lib_or_die lib => 'bz2');

$ffi->attach(
  [ BZ2_bzBuffToBuffCompress => 'compress' ] => [
    'opaque',                           # dest
    'unsigned int *',                   # dest length
    'opaque',                           # source
    'unsigned int',                     # source length
    'int',                              # blockSize100k
    'int',                              # verbosity
    'int',                              # workFactor
  ] => 'int',
  sub {
    my $sub = shift;
    my($source,$source_length) = scalar_to_buffer $_[0];
    my $dest_length = int(length($source)*1.01) + 1 + 600;
    my $dest = malloc $dest_length;
    my $r = $sub->($dest, \$dest_length, $source, $source_length, 9, 0, 30);
    die "bzip2 error $r" unless $r == 0;
    my $compressed = buffer_to_scalar($dest, $dest_length);
    free $dest;
    $compressed;
  },
);

$ffi->attach(
  [ BZ2_bzBuffToBuffDecompress => 'decompress' ] => [
    'opaque',                           # dest
    'unsigned int *',                   # dest length
    'opaque',                           # source
    'unsigned int',                     # source length
    'int',                              # small
    'int',                              # verbosity
  ] => 'int',
  sub {
    my $sub = shift;
    my($source, $source_length) = scalar_to_buffer $_[0];
    my $dest_length = $_[1];
    my $dest = malloc $dest_length;
    my $r = $sub->($dest, \$dest_length, $source, $source_length, 0, 0);
    die "bzip2 error $r" unless $r == 0;
    my $decompressed = buffer_to_scalar($dest, $dest_length);
    free $dest;
    $decompressed;
  },
);

my $original = "hello compression world\n";
my $compressed = compress($original);
print decompress($compressed, length $original);

Discussion: bzip2 is a compression library. For simple one shot attempts at compression/decompression when you expect the original and the result to fit within memory it provides two convenience functions BZ2_bzBuffToBuffCompress and BZ2_bzBuffToBuffDecompress.

The first four arguments of both of these C functions are identical, and represent two buffers. One buffer is the source, the second is the destination. For the destination, the length is passed in as a pointer to an integer. On input this integer is the size of the destination buffer, and thus the maximum size of the compressed or decompressed data. When the function returns the actual size of compressed or compressed data is stored in this integer.

This is normal stuff for C, but in Perl our buffers are scalars and they already know how large they are. In this sort of situation, wrapping the C function in some Perl code can make your interface a little more Perl like. In order to do this, just provide a code reference as the last argument to the "attach" method. The first argument to this wrapper will be a code reference to the C function. The Perl arguments will come in after that. This allows you to modify / convert the arguments to conform to the C API. What ever value you return from the wrapper function will be returned back to the original caller.

The Win32 API

use utf8;
use FFI::Platypus 2.00;

my $ffi = FFI::Platypus->new(
  api  => 2,
  lib  => [undef],
);

# see FFI::Platypus::Lang::Win32
$ffi->lang('Win32');

# Send a Unicode string to the Windows API MessageBoxW function.
use constant MB_OK                   => 0x00000000;
use constant MB_DEFAULT_DESKTOP_ONLY => 0x00020000;
$ffi->attach( [MessageBoxW => 'MessageBox'] => [ 'HWND', 'LPCWSTR', 'LPCWSTR', 'UINT'] => 'int' );
MessageBox(undef, "I ❤️ Platypus", "Confession", MB_OK|MB_DEFAULT_DESKTOP_ONLY);

Discussion: The API used by Microsoft Windows present some unique challenges. On 32 bit systems a different ABI is used than what is used by the standard C library. It also provides a rats nest of type aliases. Finally if you want to talk Unicode to any of the Windows API you will need to use UTF-16LE instead of utf-8 which is native to Perl. (The Win32 API refers to these as LPWSTR and LPCWSTR types). As much as possible the Win32 "language" plugin attempts to handle this transparently. For more details see FFI::Platypus::Lang::Win32.

bundle your own code

ffi/foo.c:

#include <ffi_platypus_bundle.h>
#include <string.h>

typedef struct {
  char *name;
  int value;
} foo_t;

foo_t*
foo__new(const char *class_name, const char *name, int value)
{
  (void)class_name;
  foo_t *self = malloc( sizeof( foo_t ) );
  self->name = strdup(name);
  self->value = value;
  return self;
}

const char *
foo__name(foo_t *self)
{
  return self->name;
}

int
foo__value(foo_t *self)
{
  return self->value;
}

void
foo__DESTROY(foo_t *self)
{
  free(self->name);
  free(self);
}

lib/Foo.pm:

package Foo;

use strict;
use warnings;
use FFI::Platypus 2.00;

{
  my $ffi = FFI::Platypus->new( api => 2 );

  $ffi->type('object(Foo)' => 'foo_t');
  $ffi->mangler(sub {
    my $name = shift;
    $name =~ s/^/foo__/;
    $name;
  });

  $ffi->bundle;

  $ffi->attach( new =>     [ 'string', 'string', 'int' ] => 'foo_t'  );
  $ffi->attach( name =>    [ 'foo_t' ]                   => 'string' );
  $ffi->attach( value =>   [ 'foo_t' ]                   => 'int'    );
  $ffi->attach( DESTROY => [ 'foo_t' ]                   => 'void'   );
}

1;

You can bundle your own C (or other compiled language) code with your Perl extension. Sometimes this is helpful for smoothing over the interface of a C library which is not very FFI friendly. Sometimes you may want to write some code in C for a tight loop. Either way, you can do this with the Platypus bundle interface. See FFI::Platypus::Bundle for more details.

Also related is the bundle constant interface, which allows you to define Perl constants in C space. See FFI::Platypus::Constant for details.

FAQ

How do I get constants defined as macros in C header files

This turns out to be a challenge for any language calling into C, which frequently uses #define macros to define constants like so:

#define FOO_STATIC  1
#define FOO_DYNAMIC 2
#define FOO_OTHER   3

As macros are expanded and their definitions are thrown away by the C pre-processor there isn't any way to get the name/value mappings from the compiled dynamic library.

You can manually create equivalent constants in your Perl source:

use constant FOO_STATIC  => 1;
use constant FOO_DYNAMIC => 2;
use constant FOO_OTHER   => 3;

If there are a lot of these types of constants you might want to consider using a tool (Convert::Binary::C can do this) that can extract the constants for you.

See also the "Integer constants" example in FFI::Platypus::Type.

You can also use the new Platypus bundle interface to define Perl constants from C space. This is more reliable, but does require a compiler at install time. It is recommended mainly for writing bindings against libraries that have constants that can vary widely from platform to platform. See FFI::Platypus::Constant for details.

What about enums?

The C enum types are integers. The underlying type is up to the platform, so Platypus provides enum and senum types for unsigned and singed enums respectively. At least some compilers treat signed and unsigned enums as different types. The enum values are essentially the same as macro constants described above from an FFI perspective. Thus the process of defining enum values is identical to the process of defining macro constants in Perl.

For more details on enumerated types see "Enum types" in FFI::Platypus::Type.

There is also a type plugin (FFI::Platypus::Type::Enum) that can be helpful in writing interfaces that use enums.

Memory leaks

There are a couple places where memory is allocated, but never deallocated that may look like memory leaks by tools designed to find memory leaks like valgrind. This memory is intended to be used for the lifetime of the perl process so there normally this isn't a problem unless you are embedding a Perl interpreter which doesn't closely match the lifetime of your overall application.

Specifically:

type cache

some types are cached and not freed. These are needed as long as there are FFI functions that could be called.

attached functions

Attaching a function as an xsub will definitely allocate memory that won't be freed because the xsub could be called at any time, including in END blocks.

The Platypus team plans on adding a hook to free some of this "leaked" memory for use cases where Perl and Platypus are embedded in a larger application where the lifetime of the Perl process is significantly smaller than the overall lifetime of the whole process.

I get seg faults on some platforms but not others with a library using pthreads.

On some platforms, Perl isn't linked with libpthreads if Perl threads are not enabled. On some platforms this doesn't seem to matter, libpthreads can be loaded at runtime without much ill-effect. (Linux from my experience doesn't seem to mind one way or the other). Some platforms are not happy about this, and about the only thing that you can do about it is to build Perl such that it links with libpthreads even if it isn't a threaded Perl.

This is not really an FFI issue, but a Perl issue, as you will have the same problem writing XS code for the such libraries.

Doesn't work on Perl 5.10.0.

I try as best as possible to support the same range of Perls as the Perl toolchain. That means all the way back to 5.8.1. Unfortunately, 5.10.0 seems to have a problem that is difficult to diagnose. Patches to fix are welcome, if you want to help out on this, please see:

https://github.com/PerlFFI/FFI-Platypus/issues/68

Since this is an older buggy version of Perl it is recommended that you instead upgrade to 5.10.1 or later.

CAVEATS

Platypus and Native Interfaces like libffi rely on the availability of dynamic libraries. Things not supported include:

Systems that lack dynamic library support

Like MS-DOS

Systems that are not supported by libffi

Like OpenVMS

Languages that do not support using dynamic libraries from other languages

This used to be the case with Google's Go, but is no longer the case. This is a problem for C / XS code as well.

Languages that do not compile to machine code

Like .NET based languages and Java.

The documentation has a bias toward using FFI / Platypus with C. This is my fault, as my background in mainly in C/C++ programmer (when I am not writing Perl). In many places I use "C" as a short form for "any language that can generate machine code and is callable from C". I welcome pull requests to the Platypus core to address this issue. In an attempt to ease usage of Platypus by non C programmers, I have written a number of foreign language plugins for various popular languages (see the SEE ALSO below). These plugins come with examples specific to those languages, and documentation on common issues related to using those languages with FFI. In most cases these are available for easy adoption for those with the know-how or the willingness to learn. If your language doesn't have a plugin YET, that is just because you haven't written it yet.

SUPPORT

IRC: #native on irc.perl.org

(click for instant chat room login)

If something does not work the way you think it should, or if you have a feature request, please open an issue on this project's GitHub Issue tracker:

https://github.com/perlFFI/FFI-Platypus/issues

CONTRIBUTING

If you have implemented a new feature or fixed a bug then you may make a pull request on this project's GitHub repository:

https://github.com/PerlFFI/FFI-Platypus/pulls

This project is developed using Dist::Zilla. The project's git repository also comes with the Makefile.PL file necessary for building, testing (and even installing if necessary) without Dist::Zilla. Please keep in mind though that these files are generated so if changes need to be made to those files they should be done through the project's dist.ini file. If you do use Dist::Zilla and already have the necessary plugins installed, then I encourage you to run dzil test before making any pull requests. This is not a requirement, however, I am happy to integrate especially smaller patches that need tweaking to fit the project standards. I may push back and ask you to write a test case or alter the formatting of a patch depending on the amount of time I have and the amount of code that your patch touches.

This project's GitHub issue tracker listed above is not Write-Only. If you want to contribute then feel free to browse through the existing issues and see if there is something you feel you might be good at and take a whack at the problem. I frequently open issues myself that I hope will be accomplished by someone in the future but do not have time to immediately implement myself.

Another good area to help out in is documentation. I try to make sure that there is good document coverage, that is there should be documentation describing all the public features and warnings about common pitfalls, but an outsider's or alternate view point on such things would be welcome; if you see something confusing or lacks sufficient detail I encourage documentation only pull requests to improve things.

The Platypus distribution comes with a test library named libtest that is normally automatically built by ./Build test. If you prefer to use prove or run tests directly, you can use the ./Build libtest command to build it. Example:

% perl Makefile.PL
% make
% make ffi-test
% prove -bv t
# or an individual test
% perl -Mblib t/ffi_platypus_memory.t

The build process also respects these environment variables:

FFI_PLATYPUS_DEBUG_FAKE32

When building Platypus on 32 bit Perls, it will use the Math::Int64 C API and make Math::Int64 a prerequisite. Setting this environment variable will force Platypus to build with both of those options on a 64 bit Perl as well.

% env FFI_PLATYPUS_DEBUG_FAKE32=1 perl Makefile.PL
DEBUG_FAKE32:
  + making Math::Int64 a prereq
  + Using Math::Int64's C API to manipulate 64 bit values
Generating a Unix-style Makefile
Writing Makefile for FFI::Platypus
Writing MYMETA.yml and MYMETA.json
%

FFI_PLATYPUS_NO_ALLOCA

Platypus uses the non-standard and somewhat controversial C function alloca by default on platforms that support it. I believe that Platypus uses it responsibly to allocate small amounts of memory for argument type parameters, and does not use it to allocate large structures like arrays or buffers. If you prefer not to use alloca despite these precautions, then you can turn its use off by setting this environment variable when you run Makefile.PL:

helix% env FFI_PLATYPUS_NO_ALLOCA=1 perl Makefile.PL
NO_ALLOCA:
  + alloca() will not be used, even if your platform supports it.
Generating a Unix-style Makefile
Writing Makefile for FFI::Platypus
Writing MYMETA.yml and MYMETA.json

V

When building platypus may hide some of the excessive output when probing and building, unless you set V to a true value.

% env V=1 perl Makefile.PL
% make V=1
...

Coding Guidelines

  • Do not hesitate to make code contribution. Making useful contributions is more important than following byzantine bureaucratic coding regulations. We can always tweak things later.
  • Please make an effort to follow existing coding style when making pull requests.
  • Platypus supports all production Perl releases since 5.8.1. For that reason, please do not introduce any code that requires a newer version of Perl.

Performance Testing

As Mark Twain was fond of saying there are four types of lies: lies, damn lies, statistics and benchmarks. That being said, it can sometimes be helpful to compare the runtime performance of Platypus if you are making significant changes to the Platypus Core. For that I use `FFI-Performance`, which can be found in my GitHub repository here:

System integrators

This distribution uses Alien::FFI in fallback mode, meaning if the system doesn't provide pkg-config and libffi it will attempt to download libffi and build it from source. If you are including Platypus in a larger system (for example a Linux distribution) you only need to make sure to declare pkg-config or pkgconf and the development package for libffi as prereqs for this module.

SEE ALSO

Extending Platypus

FFI::Platypus::Type

Type definitions for Platypus.

FFI::C

Interface for defining structured data records for use with Platypus. It supports C struct, union, nested structures and arrays of all of those. It only supports passing these types by reference or pointer, so if you need to pass structured data by value see FFI::Platypus::Record below.

FFI::Platypus::Record

Interface for defining structured data records for use with Platypus. Included in the Platypus core. Supports pass by value which is uncommon in C, but frequently used in languages like Rust and Go. Consider using FFI::C instead if you don't need to pass by value.

FFI::Platypus::API

The custom types API for Platypus.

FFI::Platypus::Memory

Memory functions for FFI.

Languages

FFI::TinyCC

JIT C compiler for FFI.

FFI::Platypus::Lang::C

Documentation and tools for using Platypus with the C programming language

FFI::Platypus::Lang::CPP

Documentation and tools for using Platypus with the C++ programming language

FFI::Platypus::Lang::Fortran

Documentation and tools for using Platypus with Fortran

FFI::Platypus::Lang::Go

Documentation and tools for using Platypus with Go

FFI::Platypus::Lang::Pascal

Documentation and tools for using Platypus with Free Pascal

FFI::Platypus::Lang::Rust

Documentation and tools for using Platypus with the Rust programming language

FFI::Platypus::Lang::ASM

Documentation and tools for using Platypus with the Assembly

FFI::Platypus::Lang::Win32

Documentation and tools for using Platypus with the Win32 API.

Wasm and Wasm::Wasmtime

Modules for writing WebAssembly bindings in Perl. This allows you to call functions written in any language supported by WebAssembly. These modules are also implemented using Platypus.

Other Tools Related Tools Useful for FFI

FFI::CheckLib

Find dynamic libraries in a portable way.

Convert::Binary::C

A great interface for decoding C data structures, including structs, enums, #defines and more.

pack and unpack

Native to Perl functions that can be used to decode C struct types.

C::Scan

This module can extract constants and other useful objects from C header files that may be relevant to an FFI application. One downside is that its use may require development packages to be installed.

Other Foreign Function Interfaces

Dyn

A wrapper around dyncall, which is itself an alternative to libffi.

NativeCall

Promising interface to Platypus inspired by Raku.

Win32::API

Microsoft Windows specific FFI style interface.

FFI

Older, simpler, less featureful FFI. It used to be implemented using FSF's ffcall. Because ffcall has been unsupported for some time, I reimplemented this module using FFI::Platypus.

C::DynaLib

Another FFI for Perl that doesn't appear to have worked for a long time.

C::Blocks

Embed a tiny C compiler into your Perl scripts.

P5NCI

Yet another FFI like interface that does not appear to be supported or under development anymore.

Other

Alien::FFI

Provides libffi for Platypus during its configuration and build stages.

ACKNOWLEDGMENTS

In addition to the contributors mentioned below, I would like to acknowledge Brock Wilcox (AWWAIID) and Meredith Howard (MHOWARD) whose work on FFI::Sweet not only helped me get started with FFI but significantly influenced the design of Platypus.

Dan Book, who goes by Grinnz on IRC for answering user questions about FFI and Platypus.

In addition I'd like to thank Alessandro Ghedini (ALEXBIO) whose work on another Perl FFI library helped drive some of the development ideas for FFI::Platypus.

AUTHOR

Author: Graham Ollis plicease@cpan.org

Contributors:

Bakkiaraj Murugesan (bakkiaraj)

Dylan Cali (calid)

pipcet

Zaki Mughal (zmughal)

Fitz Elliott (felliott)

Vickenty Fesunov (vyf)

Gregor Herrmann (gregoa)

Shlomi Fish (shlomif)

Damyan Ivanov

Ilya Pavlov (Ilya33)

Petr Písař (ppisar)

Mohammad S Anwar (MANWAR)

Håkon Hægland (hakonhagland, HAKONH)

Meredith (merrilymeredith, MHOWARD)

Diab Jerius (DJERIUS)

Eric Brine (IKEGAMI)

szTheory

José Joaquín Atria (JJATRIA)

Pete Houston (openstrike, HOUSTON)

COPYRIGHT AND LICENSE

This software is copyright (c) 2015-2022 by Graham Ollis.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.


Download Details:

Author: PerlFFI
Source Code: https://github.com/PerlFFI/FFI-Platypus

#perl 

Write Perl Bindings to Non-Perl Libraries with FFI
Rocio  O'Keefe

Rocio O'Keefe

1661554740

Expose oneTrust's Native CMP Platform to Your Flutter Project

OTPublishersNativeSDK

Expose OneTrust's Native CMP platform to your Flutter project.

Getting Started

Versioning

The SDK version used must match the version of the JSON published from your OneTrust instance. For example, if you’ve published version 6.10.0 of the JSON in your OneTrust environment, you must use the Flutter plugin version 6.10.0 as well. It is recommended to specify a version to avoid automatic updates, as a OneTrust publish is required when you update your SDK version.

To install the OneTrust plugin, run flutter pub add onetrust_publishers_native_cmp

Specify a version in your pubspec.yaml file, for example:

dependencies:
  onetrust_publishers_native_cmp: '6.10.0'

After adding the plugin to your Flutter project, build the project to check for any immediate errors.

This plugin pulls the OneTrust SDK from mavenCentral and Cocoapods.

Notes

OTPublishersNativeSDK supports a minimum iOS version of 11.0. You may have to specify a platform target in your podfile:

platform :ios, '11.0'

In Android, the underlying Native SDK requires a FragmentActivity in order to render a UI. In your Android MainActivity.java file, ensure that the MainActivity class is extending FlutterFragmentActivity.

Additionally, Android must use targetSdkVersion 31 and compileSDKVersion 31 or higher.

public class MainActivity extends FlutterFragmentActivity {
}

Usage

Initialization

Initialize OneTrust to fetch the data configured in your OneTrust tenant. This will make one or two network calls (depending on the type of template) and deliver a JSON object that contains all of the information required to render a banner and preference center.

The init call is an async function that returns a boolean status value; true if the initialization and download of data was successful, false if not.

Future<void> initOneTrust() async{
    bool status;
    Map<String, String> params = { //Params are not required
      "countryCode":"US",
      "regionCode":"CA"
    }
    try{
      status = await OTPublishersNativeSDK.startSDK("cdn.cookielaw.org","dec6b152-4ad9-487b-8e5a-24a06298417f","en", params);
    } on PlatformException{
      print("Error communicating with platform-side code");
    }

    if (!mounted) return;

    setState(() {
      _cmpDownloadStatus = status ? 'Success!':'Error';
    });

  }

Arguments

NameTypeDescription
storageLocationString[Required] The CDN location for the JSON that the SDK fetches. (Usually, but not always, cdn.cookielaw.org.)
domainIdentifierString[Required] The Application guid (retrieved from OneTrust Admin console)
languageCodeString[Required] 2-digit ISO language code used to return content in a specific langauge.

Note: Any language code format which is not listed in OneTrust environment will be considered as invalid input.

Note: If the languageCode passed by your application is valid, but does not match with a language configuration for your template, then the SDK will return content in the default language configured in OneTrust environment (usually, but not always, English).
paramsMap<String, String>Parameter map (see below for accepted values)

Initialization Parameters

All initialization parameters are expected to be of type String, and all are optional.

NameTypeDescription
countryCodeString2-digit ISO country code that will override OneTrust's geolocation service for locating the user. Typically used for QA to test other regions, or if your application knows the user's location applicable for consent purposes.
regionCodeString2-digit ISO region code that will overide OneTrust's geolocation service.
androidUXParamsStringA stringified representation of the OTUXParams JSON object to override styling in-app. See "Android - Custom Styling with UXParams JSON" below.

Show an Interface

The plugin can load a Banner or a Preference center using the methods below:

OTPublishersNativeSDK.showBannerUI(); //load Banner
OTPublishersNativeSDK.showPreferenceCenterUI(); //load Preference Center

To determine if a banner should be shown (based on the template rendered and the status of the user's current consent,) use the shouldShowBanner() method. This returns a bool indicating whether or not a banner should be shown.

bool _shouldShowBanner;
try{
      _shouldShowBanner = await OTPublishersNativeSDK.shouldShowBanner();
    } on PlatformException{
      print("Error communicating with platform-side code");
    }
    if(_shouldShowBanner){
      OTPublishersNativeSDK.showBannerUI();
    }

Permission Prompts (App Tracking Transparency and Age Gate)

A permission prompt can be displayed for several reasons. Currently, two prompts are supported on Flutter: the App Tracking Transparency pre-prompt and the Age Gate prompt.

To surface these prompts, ensure that they have been configured in your OneTrust template. The prompts are surfaced by calling showConsentUI and passing in an OTDevicePermission enum. An integer is returned that indicates the result of the prompt.

//IDFA/ATT - iOS Only
 int authStatus = await OTPublishersNativeSDK.showConsentUI(OTDevicePermission.idfa);
 //Use the following to get the status as an enum -- there are several outcomes for IDFA specifically
 OTATTrackingAuthorizationStatus IDFAStatus = OTATTrackingAuthorizationStatus.values[authStatus];

 //Age Gate - iOS and Android
 int authStatus = await OTPublishersNativeSDK.showConsentUI(OTDevicePermission.AgeGate);
 //returns a 0 (user responded no) or a 1 (user responded yes)

This method will not resolve until the user has made a selection for the permission prompt..

This method takes an argument of type OTDevicePermission, which is provided as an enum for ease of use. It returns an OTATTrackingAuthorizationStatus.

Query ATT Status

The current status of App Tracking Transparency can be obtained by calling

OTATTrackingAuthorizationStatus authStatus = await OTPublishersNativeSDK.getATTrackingAuthorizationStatus();

The above method returns an OTATTTrackingAuthorizationStatus which is namespaced as such to avoid issues with other ATT plugins being used. For unsupported OS versions (all Android versions and iOS versions < 14,) this function will immediately return platformNotSupported. Possible values of the enum are

  • notDetermined
  • restricted
  • denied
  • authorized
  • platformNotSupported

Query for Age Gate Status

The current status of the Age Gate can be obtained by calling

int authStatus = await OTPublishersNativeSDK.getAgeGatePromptValue()

This method will return a 0 for a No response to the Age Gate and a 1 for a Yes response.

Android - Custom Styling with UXParams JSON

OneTrust allows you to add custom styling to your preference center by passing in style JSON in a certain format. Build out your JSON by following the guide in the OneTrust Developer Portal.

Pass the JSON as a string into the startSDK function's params argument.

  String AndroidUXParams = await DefaultAssetBundle.of(context)
        .loadString("assets/AndroidUXParams.json");
    Map<String, String> params = {
      "androidUXParams": AndroidUXParams
    };
    try {
      status = await OTPublishersNativeSDK.startSDK(
          "cdn.cookielaw.org", appId, "en", params);
    } on PlatformException {
      print("Error communicating with platform code");
    }

iOS - Custom Styling with UXParams Plist

Custom styling can be added to your iOS Cordova application by using a .plist file in the iOS platform code. In addition to adding the .plist file (which can be obtained from the OneTrust Demo Application) to your bundle, there are a few changes that need to be made in the platform code, outlined below. Review the guide in the OneTrust Developer Portal.

In AppDelegate.swift, import OTPublishersHeadlessSDK. Add an extension below the class to handle the protocol methods:

import OTPublishersHeadlessSDK
...
extension AppDelegate: UIConfigurator{
    func shouldUseCustomUIConfig() -> Bool {
        return true
    }
    
    func customUIConfigFilePath() -> String? {
        return Bundle.main.path(forResource: "OTSDK-UIConfig-iOS", ofType: "plist")
    }
}

In the didFinishLaunchingWithOptions protocol method, assign the AppDelegate as the UIConfigurator:

@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    OTPublishersHeadlessSDK.shared.uiConfigurator = self //add this line only
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

Listening for UI Changes

The plugin implements an EventChannel that opens a BroadcastStream with the platform-side code. To listen for changes in the ui.

Note that:

  • The BroadcastStream will stay open until it is closed by calling .cancel() on it
  • Only one BroadcastStream per EventChannel can be open at a single time. Therefore, you must call .cancel() on this stream before calling .listenForUIInteractions().listen again.
var interactionListener = OTPublishersNativeSDK.listenForUIInteractions().listen((event) {
      print(event);
      });

Expected return:

{
    'uiEvent':String,
    'payload':Object
}

UI Events

Event NameDescriptionPayload
onShowBannerTriggered when banner is shownnull
onHideBannerTriggered when banner is closednull
onBannerClickedAcceptAllTriggered when user allows all consent from bannernull
onBannerClickedRejectAllTriggered when user rejects all consent from bannernull
onShowPreferenceCenterTriggered when Preference Center is displayednull
onHidePreferenceCenterTriggered when Preference Center is closednull
onPreferenceCenterAcceptAllTriggered when user allows all consent from Preference Centernull
onPreferenceCenterRejectAllTriggered when user rejects all consent from Preference Centernull
onPreferenceCenterConfirmChoicesTriggered when user clicked on save choices after updating consent values from Preference Centernull
onShowVendorListTriggered when vendor list UI is displayed from an IAB banner/ IAB Preference centernull
onHideVendorListTriggered when vendor list UI is closed or when back button is clickednull
onVendorConfirmChoicesTriggered when user updates vendor consent / legitimate interests purpose values and save the choices from vendor listnull
onVendorListVendorConsentChangedTriggered when user updates consent values for a particular vendor id on vendor list UI{vendorId:String, consentStatus:Int}
onVendorListVendorLegitimateInterestChangedTriggered when user updates Legitimate interests values for a particular vendor id on vendor list UI{vendorId:String, legitInterest:Int}
onPreferenceCenterPurposeConsentChangedTriggered when user updates consent values for a particular category on Preference Center UI{purposeId:String, consentStatus:Int}
onPreferenceCenterPurposeLegitimateInterestChangedTriggered when user updates Legitimate interest values for a particular category on Preference Center UI{purposeId:String, legitInterest:Int}
allSDKViewsDismissedTriggered when all the OT SDK Views are dismissed from the view hierarchy.{interactionType:String}

When Consent Changes

The consent status is returned as an integer value: |Status|Description| |-|-| |1|Consent given| |0|Consent not given| |-1|Consent not gathered or SDK not initialized|

 

Query for Consent

To get the present consent state for a category, you can query for consent:

Future<void> getConsentStatus() async{
    int status;
    try{
      status = await OTPublishersNativeSDK.getConsentStatusForCategory("C0002");
    } on PlatformException{
      print("Error communicating with platform-side code.");
    }
    print("Queried Status for C0002 is = "+status.toString());
  }

Listen for Consent Changes

The plugin implements an EventChannel that opens a BroadcastStream with the platform-side code. To listen for changes to the consent state.

Note that:

  • The BroadcastStream will stay open until it is closed by calling .cancel() on it
  • Only one BroadcastStream per EventChannel can be open at a single time. Therefore, you must call .cancel() on this stream before calling .listenForConsentChanges().listen again.
var consentListener = OTPublishersNativeSDK.listenForConsentChanges(["C0002","C0003"]).listen((event) {
      setCategoryState(event['categoryId'], event['consentStatus']);
      print("New status for "+event['categoryId']+" is "+event['consentStatus'].toString());
    });

    //consentListener.cancel() //cancel listener

This listener accepts an array of strings containing Category IDs to listen for. The listener returns an object:

{
    'categoryId':String
    'consentStatus':int
}

Get Current Data Subject Identifier

OneTrust sets a GUID to identify the user for audit purposes if one is not specified. The identifier of the current suer can be used to look up the user's consent history in the OneTrust Consent module. To surface the identifier:

String id = await OTPublishersNativeSDK.getCurrentActiveProfile();

Inject Consent to WebView

If you use WebViews in your applications, you may encounter a situation where you render a webpage running OneTrust’s Cookie Consent module, and the cookie banner is displayed. OneTrust provides a native way to take the consent gathered from the Native SDK and pass it down to the WebView.

To do this, simply call the function below to request a JavaScript variable from the OneTrust SDK. Placing this on the OneTrust SDK before the Cookies SDK loads will suppress the banner and pass consent to the WebView.

String jsToPass = await OTPublishersNativeSDK.getOTConsentJSForWebview();

Build Your Own UI

The OneTrust SDK offers several methods that can be leveraged to build your own interface.

Get Domain Info

This method returns a map containing all the information about the domain (application) including rulesets and template configurations.

Map<String, dynamic> domainInfo = await OTPublishersNativeSDK.getDomainInfo();

Get Common Data

This method returns a map which contains template based information like branding which includes keys for determining colors and styles in the UI specific to a template configured for the user's geolocation along with consent logging information.

Map<String, dynamic> commonData = await OTPublishersNativeSDK.getCommonData();

Get Groups Data

This method returns a dictionary containing the information about all the categories and sdk IDs specific to a template configured for the user's geolocation. This is generally used to populate the preference center's toggle section.

Map<String, dynamic> groups = await OTPublishersNativeSDK.getDomainGroupData();

Get Banner Data

This method returns a dictionary which contains all the keys required to render a banner.

Map<String, dynamic> bannerData = await OTPublishersNativeSDK.getBannerData();

Update Purpose Consent

This method is used to modify the consent status of a category by passing Custom Group ID as the key and consent status as value. Note that you have to call 'saveConsent(OTInteractionType)', with type being ConsentInteractionType, to have any modifications you perform to actually register in OneTrust SDK. OneTrust will notify the consent status changes to your app on saving changes.

  //A user has just toggled on Category C0002
  OTPublishersNativeSDK.updatePurposeConsent("C0002", true);
  //After the user has made all of their selections and taps save, execute the saveConsent method below

Save Consent

Call this method to save the user's consent when they've indicated they wish for their consent to take effect. Examples include calling this method when a user accepts all on the banner, or after they've made all of their selections inside of a preference center.

//User confirms their choices in Pref Center
OTPublishersNativeSDK.saveConsent(OTInteractionType.preferenceCenterConfirm);

//User accepts all on the banner
OTPublishersNativeSDK.saveConsent(OTInteractionType.bannerAllowAll);

If configured, this will cause a record of consent to be sent. It will also signal the choices to the rest of your application via listeners.

The OTInteractionType is a convenient way to keep track of interaction types. There are many, but the most commonly used are |Enum Name|Description| |-|-| |bannerAllowAll|User has accepted all on the banner.| |bannerRejectAll|User has rejected all on the banner.| |bannerContinueWithoutAccepting|User has selected the "Continue Without Accepting" CNIL button on the banner.| |bannerClose|User has closed the banner without making a selection.| |preferenceCenterAllowAll|User has selected Allow All in the preference center.| |preferenceCenterRejectAll|User has selected Reject All in the preference center.| |preferenceCenterConfirm|User has made selections in the Preference Center and tapped Confirm.|

Reset Local Consent

If a user has made selections in the preference center but then closes the preference center without saving, call this method to abandon the "staged" consent changes.

OTPublishersNativeSDK.resetUpdatedConsent();

Universal Consent

Universal consent allows your application to collect consent for off-device processing. For example, your application might need to request permission to send a user promotional SMS messages. In OneTrust, after a Universal Consent transaction is committed, integration workflows can be triggered in the OneTrust tool.

Universal Consent requires a user identifier to work properly.

Display Universal Consent Preference Center

To load the Universal Consent preference center over the current screen, simply call

OTPublishersNativeSDK.showUCPurposesUI()

Query for Universal Consent Values

This plugin exposes async methods to retrieve the current state of the users' consent.

StatusExplanation
1Consent Given
0Consent Not Given
-1Consent not yet gathered, or SDK not initialized

To query for the top-level purpose's consent:

int? consent = await OTPublishersNativeSDK.getUCPurposeConsent('purposeId')
ArgumentTypeDescription
purposeIdStringThe GUID of the purpose to retrieve.

To query for a custom preference nested under a purpose:

int? consent = await OTPublishersNativeSDK.getUCPurposeCustomPreferenceOptionConsent('customPreferenceOptionId',
 'customPreferenceId','purposeId')
ArgumentTypeDescription
customPreferenceOptionIdStringThe GUID of the custom preference option
customPreferenceIdStringThe GUID of the custom preference group
purposeIdStringThe GUID of the purpose under which the custom preference is nested.

To query for a topic nested under a purpose:

int? consent = await OTPublishersNativeSDK.getUCPurposeTopicConsent('topicOptionId', 'purposeId')
ArgumentTypeDescription
topicOptionIdStringThe GUID of the topic option
purposeIdStringThe GUID of the purpose under which the custom preference is nested.

Programatically Set Universal Consent Values

The package exposes methods to programatically set the users' consent values. A common use case is when a sign-up form has a checkbox at the bottom to allow the user to opt into emails. In this case, the application would set the value of the associated purpose, and the user could change his or her decision later in the preference center.

After making consent updates, the application must call the saveUCConsent() function to commit the changes.

To update the top-level purpose's consent:

await OTPublishersNativeSDK.updateUCPurposeConsent('purposeId', true)
ArgumentTypeDescription
purposeIdStringThe GUID of the purpose to update
consentBooleanWhether or not consent has been granted for the specified item

To update a custom preference nested under a purpose:

OTPublishersNativeSDK.updateUCPurposeCutomPreferenceOptionConsent('customPreferenceOptionId',
 'customPreferenceId','purposeId', true)
ArgumentTypeDescription
topicIdStringThe GUID of the custom preference option
purposeIdStringThe GUID of the custom preference group
consentBooleanWhether or not consent has been granted for the specified item

To update a topic nested under a purpose:

OTPublishersNativeSDK.updateUCPurposeTopicConsent('topicOptionId', 'purposeId', true)

Save Consent

After making updates to the consent values, the application must call the following to commit the changes:

  OTPublishersNativeSDK.saveConsent(OTInteractionType.ucPreferenceCenterConfirm)

Resolving Dependency Clashes

In rare cases, one of the dependencies required by OneTrust's underlying Native Android SDK may clash with dependencies in your application. Most of the time, Gradle will resolve these automatically. If that is not the case however, OneTrust provides the option to suppress all transitive dependencies from the OneTrust .aar file.

To suppress all transitive dependencies, add onetrust.includeTransitiveDependencies=false to your Android project's local.properties file. If your application suppresses OneTrust's dependencies, the dependencies must be added to your project manually. A list of required dependencies is available at Developer.OneTrust.com.

Use this package as a library

Depend on it

Run this command:

With Flutter:

 $ flutter pub add onetrust_publishers_native_cmp

This will add a line like this to your package's pubspec.yaml (and run an implicit flutter pub get):

dependencies:
  onetrust_publishers_native_cmp: ^6.39.0

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

Import it

Now in your Dart code, you can use:

import 'package:onetrust_publishers_native_cmp/onetrust_publishers_native_cmp.dart';

example/lib/main.dart

import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:io' show Platform;

import 'package:flutter/services.dart';
import 'package:onetrust_publishers_native_cmp/onetrust_publishers_native_cmp.dart';

import 'debugMenu.dart';

void main() {
  runApp(const MaterialApp(title: "OneTrust Flutter Demo", home: HomePage()));
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  String _cmpDownloadStatus = 'Waiting';
  int _c0003Status = -1;
  int _c0004Status = -1;
  OTATTrackingAuthorizationStatus _attStatus =
      OTATTrackingAuthorizationStatus.notDetermined;
  String _dataSubjectId = "Unknown";
  @override
  void initState() {
    super.initState();
    initOneTrust();
    startListening();
  }

  Future<void> initOneTrust() async {
    bool? status;
    String appId;
    bool? shouldShowBanner;
    String? id;
    OTATTrackingAuthorizationStatus startupATTStatus;

// Currently App ID is from qa of 6.39.0
// Change this while upgrading to verify if everything is working fine with upgrades.
    if (Platform.isAndroid) {
      appId = "d572dec8-4990-4452-bcdd-ad94a2f0d827";
    } else if (Platform.isIOS) {
      appId = "d572dec8-4990-4452-bcdd-ad94a2f0d827";
    } else {
      Exception("Platform not found!");
      return;
    }
    String AndroidUXParams = await DefaultAssetBundle.of(context)
        .loadString("assets/AndroidUXParams.json");
    Map<String, String> params = {
      "countryCode": "US",
      "regionCode": "GA"
      //"androidUXParams": AndroidUXParams
    };
    try {
      status = await OTPublishersNativeSDK.startSDK("qa", appId, "en", params);
      shouldShowBanner = await OTPublishersNativeSDK.shouldShowBanner();
      id = await OTPublishersNativeSDK.getCurrentActiveProfile();
    } on PlatformException {
      print("Error communicating with platform code");
    }

    startupATTStatus =
        await OTPublishersNativeSDK.getATTrackingAuthorizationStatus();

    if (status! && shouldShowBanner!) {
      OTPublishersNativeSDK.showBannerUI();
    }

    if (!mounted) return;

    setState(() {
      _cmpDownloadStatus = status! ? 'Success!' : 'Error';
      _dataSubjectId = id!;
      _attStatus = startupATTStatus;
    });
  }

  void startListening() {
    var consentListener =
        OTPublishersNativeSDK.listenForConsentChanges(["C0003", "C0004"])
            .listen((event) {
      setCategoryState(event['categoryId'], event['consentStatus']);
      print("New status for " +
          event['categoryId'] +
          " is " +
          event['consentStatus'].toString());
    });

    var interactionListener =
        OTPublishersNativeSDK.listenForUIInteractions().listen((event) {
      print(event);
    });

    //consentListener.cancel(); //Cancel event stream before opening a new one
  }

  void setCategoryState(String category, int status) {
    setState(() {
      switch (category) {
        case "C0003":
          _c0003Status = status;
          break;
        case "C0004":
          _c0004Status = status;
          break;
        default:
          break;
      }
    });
  }

  void loadATTPrompt() async {
    int? status;
    status = await OTPublishersNativeSDK.showConsentUI(OTDevicePermission.idfa);
    if (status != null) {
      setState(() {
        _attStatus = OTATTrackingAuthorizationStatus.values[status!];
      });
    }
  }

  void loadAgeGatePrompt() async {
    int? status =
        await OTPublishersNativeSDK.showConsentUI(OTDevicePermission.ageGate);
    print("Age Gate Status is $status");
  }

  Column getATTColumn() {
    return Column(
      children: [
        ElevatedButton(
            onPressed: () {
              loadATTPrompt();
            },
            child: Text("Load ATT Prompt")),
        Text("ATT Status = $_attStatus\n")
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: Center(
        child: Column(
          children: [
            Text('OneTrust Download Status: $_cmpDownloadStatus\n'),
            ElevatedButton(
                onPressed: () {
                  OTPublishersNativeSDK.showBannerUI();
                },
                child: Text("Load Banner")),
            ElevatedButton(
                onPressed: () {
                  OTPublishersNativeSDK.showPreferenceCenterUI();
                },
                child: Text("Load Preference Center")),
            ElevatedButton(
                onPressed: () {
                  OTPublishersNativeSDK.showUCPurposesUI();
                },
                child: Text("Load UC Purposes")),
            ElevatedButton(
                onPressed: () {
                  loadAgeGatePrompt();
                },
                child: Text("Load Age Gate")),
            Platform.isIOS //conditionally render ATT Pre-prompt button
                ? getATTColumn()
                : Container(),
            Text('Category C0003 Status: $_c0003Status\n'),
            Text('Category C0004 Status: $_c0004Status\n'),
            Text('Data Subject Identifier is'),
            Text(_dataSubjectId, style: TextStyle(fontWeight: FontWeight.bold)),
            ElevatedButton(
                onPressed: () {
                  Navigator.push(
                    context,
                    MaterialPageRoute(builder: (context) => const EvenMore()),
                  );
                },
                child: Text("More")),
          ],
        ),
      ),
    );
  }
}

Original article source at: https://pub.dev/packages/onetrust_publishers_nativenative_cmp 

#flutter #dart #native #sdk 

Expose oneTrust's Native CMP Platform to Your Flutter Project
Lawrence  Lesch

Lawrence Lesch

1659664140

List Of JavaScript Methods Which You Can Use Natively + ESLint Plugin

You don't (may not) need Lodash/Underscore

Lodash and Underscore are great modern JavaScript utility libraries, and they are widely used by Front-end developers. However, when you are targeting modern browsers, you may find out that there are many methods which are already supported natively thanks to ECMAScript5 [ES5] and ECMAScript2015 [ES6]. If you want your project to require fewer dependencies, and you know your target browser clearly, then you may not need Lodash/Underscore.

You are welcome to contribute with more items provided below.

If you are targeting legacy JavaScript engine with those ES5 methods, you can use es5-shim

Please note that, the examples used below are just showing you the native alternative of performing certain tasks. For some functions, Lodash provides you more options than native built-ins. This list is not a 1:1 comparison.

Please send a PR if you want to add or modify the code. No need to open an issue unless it's something big and you want to discuss.

Voice of Developers

Make use of native JavaScript object and array utilities before going big.

—Cody Lindley, Author of jQuery Cookbook and JavaScript Enlightenment

You probably don't need Lodash. Nice List of JavaScript methods which you can use natively.

—Daniel Lamb, Computer Scientist, Technical Reviewer of Secrets of the JavaScript Ninja and Functional Programming in JavaScript

I guess not, but I want it.

—Tero Parviainen, Author of build-your-own-angular

I'll admit, I've been guilty of overusing #lodash. Excellent resource.

—@therebelrobot, Maker of web things, Facilitator for Node.js/io.js

ESLint Plugin    

If you're using ESLint, you can install a plugin that will help you identify places in your codebase where you don't (may not) need Lodash/Underscore.

Install the plugin...

npm install --save-dev eslint-plugin-you-dont-need-lodash-underscore

...then update your config

"extends" : ["plugin:you-dont-need-lodash-underscore/compatible"],

For more information, see Configuring the ESLint Plugin

Important: Note that, while many Lodash methods are null safe (e.g. _.keys, _.entries), this is not necessarily the case for their Native equivalent. If null safety is critical for your application, we suggest that you take extra precautions [e.g. consider using the native Object.keys as Object.keys(value || {})].

Collection*

Important: Note that most native equivalents are array methods, and will not work with objects. If this functionality is needed and no object method is provided, then Lodash/Underscore might be the better option. Bear in mind however, that all iterable objects can easily be converted to an array by use of the spread operator.

Array

_.chunk

Creates an array of elements split into groups the length of size.

// Underscore/Lodash
_.chunk(['a', 'b', 'c', 'd'], 2);
// => [['a', 'b'], ['c', 'd']]

_.chunk(['a', 'b', 'c', 'd'], 3);
// => [['a', 'b', 'c'], ['d']]


// Native

const chunk = (input, size) => {
  return input.reduce((arr, item, idx) => {
    return idx % size === 0
      ? [...arr, [item]]
      : [...arr.slice(0, -1), [...arr.slice(-1)[0], item]];
  }, []);
};

chunk(['a', 'b', 'c', 'd'], 2);
// => [['a', 'b'], ['c', 'd']]

chunk(['a', 'b', 'c', 'd'], 3);
// => [['a', 'b', 'c'], ['d']]

Browser Support for Spread in array literals

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
46.0 ✔12.0 ✔16.0 ✔37.0 ✔8.0 ✔

⬆ back to top

_.compact

Creates an array with all falsy values removed.

// Underscore/Lodash
_.compact([0, 1, false, 2, '', 3]);

// Native
[0, 1, false, 2, '', 3].filter(Boolean)

Browser Support for array.prototype.filter

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.5 ✔9.0 ✔

⬆ back to top

_.concat

Creates a new array concatenating array with any additional arrays and/or values.

// Underscore/Lodash
var array = [1]
var other = _.concat(array, 2, [3], [[4]])

console.log(other)
// output: [1, 2, 3, [4]]

// Native
var array = [1]
var other = array.concat(2, [3], [[4]])

console.log(other)
// output: [1, 2, 3, [4]]

Browser Support for Array.prototype.concat()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.0 ✔1.0 ✔5.5 ✔

⬆ back to top

_.difference

Similar to without, but returns the values from array that are not present in the other arrays.

// Underscore/Lodash
console.log(_.difference([1, 2, 3, 4, 5], [5, 2, 10]))
// output: [1, 3, 4]

// Native
var arrays = [[1, 2, 3, 4, 5], [5, 2, 10]];
console.log(arrays.reduce(function(a, b) {
  return a.filter(function(value) {
    return !b.includes(value);
  });
}));
// output: [1, 3, 4]

// ES6
let arrays = [[1, 2, 3, 4, 5], [5, 2, 10]];
console.log(arrays.reduce((a, b) => a.filter(c => !b.includes(c))));
// output: [1, 3, 4]

Browser Support for Array.prototype.reduce()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
3.0 ✔9.0 ✔10.5 ✔4.0 ✔

⬆ back to top

_.drop

Creates a slice of array with n elements dropped from the beginning.

// Underscore/Lodash
_.drop([1, 2, 3]);
// => [2, 3]

_.drop([1, 2, 3], 2);
// => [3]

// Native
[1, 2, 3].slice(1);
// => [2, 3]

[1, 2, 3].slice(2);
// => [3]

Browser Support for Array.prototype.slice()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.0 ✔1.0 ✔

⬆ back to top

_.dropRight

Creates a slice of array with n elements dropped at the end.

// Underscore/Lodash
_.dropRight([1, 2, 3]);
// => [1, 2]

_.dropRight([1, 2, 3], 2);
// => [1]

// Native
[1, 2, 3].slice(0, -1);
// => [1, 2]

[1, 2, 3].slice(0, -2);
// => [1]

Browser Support for Array.prototype.slice()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.0 ✔1.0 ✔

⬆ back to top

_.fill

Fills elements of array with value from start up to, but not including, end. Note that fill is a mutable method in both native and Lodash/Underscore.

// Underscore/Lodash
var array = [1, 2, 3]

_.fill(array, 'a')

console.log(array)
// output: ['a', 'a', 'a']

_.fill(Array(3), 2)
// output: [2, 2, 2]

_.fill([4, 6, 8, 10], '*', 1, 3)
// output: [4, '*', '*', 10]

// Native
var array = [1, 2, 3]

array.fill('a')

console.log(array)
// output: ['a', 'a', 'a']

Array(3).fill(2)
// output: [2, 2, 2]

[4, 6, 8, 10].fill('*', 1, 3)
// output: [4, '*', '*', 10]

Browser Support for Array.prototype.fill()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
45.0 ✔31.0 ✔32.0 ✔8 ✔

⬆ back to top

_.find

Returns the value of the first element in the array that satisfies the provided testing function. Otherwise undefined is returned.

// Underscore/Lodash
var users = [
  { 'user': 'barney',  'age': 36, 'active': true },
  { 'user': 'fred',    'age': 40, 'active': false },
  { 'user': 'pebbles', 'age': 1,  'active': true }
]

_.find(users, function (o) { return o.age < 40; })
// output: object for 'barney'

// Native
var users = [
  { 'user': 'barney',  'age': 36, 'active': true },
  { 'user': 'fred',    'age': 40, 'active': false },
  { 'user': 'pebbles', 'age': 1,  'active': true }
]

users.find(function (o) { return o.age < 40; })
// output: object for 'barney'

Browser Support for Array.prototype.find()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
45.0 ✔25.0 ✔32.0 ✔7.1 ✔

⬆ back to top

_.findIndex

Returns the index of the first element in the array that satisfies the provided testing function. Otherwise -1 is returned.

// Underscore/Lodash
var users = [
  { 'user': 'barney',  'age': 36, 'active': true },
  { 'user': 'fred',    'age': 40, 'active': false },
  { 'user': 'pebbles', 'age': 1,  'active': true }
]

var index = _.findIndex(users, function (o) { return o.age >= 40; })
console.log(index)
// output: 1

// Native
var users = [
  { 'user': 'barney',  'age': 36, 'active': true },
  { 'user': 'fred',    'age': 40, 'active': false },
  { 'user': 'pebbles', 'age': 1,  'active': true }
]

var index = users.findIndex(function (o) { return o.age >= 40; })
console.log(index)
// output: 1

Browser Support for Array.prototype.findIndex()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
45.0 ✔25.0 ✔32.0 ✔7.1 ✔

⬆ back to top

_.first

Returns the first element of an array. Passing n will return the first n elements of the array.

// Underscore/Lodash
_.first([1, 2, 3, 4, 5]);
// => 1

// Underscore
_.first([1, 2, 3, 4, 5], 2);
// => [1, 2]

// Native
[1, 2, 3, 4, 5][0];
// => 1
//or
[].concat(1, 2, 3, 4, 5).shift()
// => 1
//or
[].concat([1, 2, 3, 4, 5]).shift()
// => 1

// Native (works even with potentially undefined/null, like _.first)
[].concat(undefined).shift()
// => undefined

[1, 2, 3, 4, 5].slice(0, 2);
// => [1, 2]

Browser Support for Array.prototype.slice()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.0 ✔1.0 ✔

⬆ back to top

_.flatten

Flattens array a single level deep.

// Underscore/Lodash
_.flatten([1, [2, [3, [4]], 5]]);
// => [1, 2, [3, [4]], 5]

// Native
const flatten = [1, [2, [3, [4]], 5]].reduce( (a, b) => a.concat(b), [])
// => [1, 2, [3, [4]], 5]

const flatten = [].concat(...[1, [2, [3, [4]], 5]])
// => [1, 2, [3, [4]], 5]

// Native(ES2019)
const flatten = [1, [2, [3, [4]], 5]].flat()
// => [1, 2, [3, [4]], 5]

const flatten = [1, [2, [3, [4]], 5]].flatMap(number => number)
// => [1, 2, [3, [4]], 5]

Browser Support for Array.prototype.reduce()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
46.0 ✔3.0 ✔9.0 ✔10.5 ✔4 ✔

Browser Support for Array.prototype.flat()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
69 ✔62 ✔56 ✔12 ✔

Browser Support for Array.prototype.flatMap()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
69 ✔62 ✔56 ✔12 ✔

⬆ back to top

_.flattenDeep

Recursively flattens array.

// Underscore/Lodash
_.flattenDeep([1, [2, [3, [4]], 5]]);
// => [1, 2, 3, 4, 5]

// Native
const flattenDeep = (arr) => Array.isArray(arr)
  ? arr.reduce( (a, b) => a.concat(flattenDeep(b)) , [])
  : [arr]

flattenDeep([1, [[2], [3, [4]], 5]])
// => [1, 2, 3, 4, 5]

// Native(ES2019)
[1, [2, [3, [4]], 5]].flat(Infinity)
// => [1, 2, 3, 4, 5]

const flattenDeep = (arr) => arr.flatMap((subArray, index) => Array.isArray(subArray) ? flattenDeep(subArray) : subArray)

flattenDeep([1, [[2], [3, [4]], 5]])
// => [1, 2, 3, 4, 5]

Browser Support

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
46.0 ✔16.0 ✔37.0 ✔7.1 ✔

Browser Support for Array.prototype.flat()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
69 ✔62 ✔56 ✔12 ✔

Browser Support for Array.prototype.flatMap()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
69 ✔62 ✔56 ✔12 ✔

⬆ back to top

_.fromPairs

Returns an object composed from key-value pairs.

// Underscore/Lodash
_.fromPairs([['a', 1], ['b', 2]]);
// => { 'a': 1, 'b': 2 }

// Native
const fromPairs = function(arr) {
  return arr.reduce(function(accumulator, value) {
    accumulator[value[0]] = value[1];
    return accumulator;
  }, {})
}

// Compact form
const fromPairs = (arr) => arr.reduce((acc, val) => (acc[val[0]] = val[1], acc), {})

fromPairs([['a', 1], ['b', 2]]);
// => { 'a': 1, 'b': 2 }

// Native(ES2019)
Object.fromEntries([['a', 1], ['b', 2]])
// => { 'a': 1, 'b': 2 }

Browser Support for Array.prototype.reduce()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
3.0 ✔9.0 ✔10.5 ✔4.0 ✔

Browser Support for Object.fromEntries()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
73.0 ✔79.0 ✔63.0 ✔60 ✔12.1 ✔

⬆ back to top

_.head and _.tail

Gets the first element or all but the first element.

const array = [1, 2, 3]

// Underscore: _.first, _.head, _.take
// Lodash: _.first, _.head
_.head(array)
// output: 1

// Underscore: _.rest, _.tail, _.drop
// Lodash: _.tail
_.tail(array)
// output: [2, 3]


// Native
const [ head, ...tail ] = array
console.log(head)
// output: 1
console.log(tail)
// output [2, 3]

Browser Support for Spread in array literals

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
46.0 ✔12.0 ✔16.0 ✔37.0 ✔8.0 ✔

⬆ back to top

_.indexOf

Returns the first index at which a given element can be found in the array, or -1 if it is not present.

// Underscore/Lodash
var array = [2, 9, 9]
var result = _.indexOf(array, 2)
console.log(result)
// output: 0

// Native
var array = [2, 9, 9]
var result = array.indexOf(2)
console.log(result)
// output: 0

Browser Support for Array.prototype.indexOf()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.5 ✔9.0 ✔

⬆ back to top

_.intersection

Returns an array that is the intersection of all the arrays. Each value in the result is present in each of the arrays.

// Underscore/Lodash
console.log(_.intersection([1, 2, 3], [101, 2, 1, 10], [2, 1]))
// output: [1, 2]

// Native
var arrays = [[1, 2, 3], [101, 2, 1, 10], [2, 1]];
console.log(arrays.reduce(function(a, b) {
  return a.filter(function(value) {
    return b.includes(value);
  });
}));
// output: [1, 2]

// ES6
let arrays = [[1, 2, 3], [101, 2, 1, 10], [2, 1]];
console.log(arrays.reduce((a, b) => a.filter(c => b.includes(c))));
// output: [1, 2]

Browser Support for Array.prototype.reduce()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
3.0 ✔9.0 ✔10.5 ✔4.0 ✔

⬆ back to top

_.takeRight

Creates a slice of array with n elements taken from the end. :heavy_exclamation_mark: Native slice does not behave entirely the same as the Lodash method. See example below to understand why.

// Underscore/Lodash
_.takeRight([1, 2, 3]);
// => [3]

_.takeRight([1, 2, 3], 2);
// => [2, 3]

_.takeRight([1, 2, 3], 5);
// => [1, 2, 3]

// Native
[1, 2, 3].slice(-1);
// => [3]

[1, 2, 3].slice(-2);
// => [2, 3]

[1, 2, 3].slice(-5);
// => [1, 2, 3]

// Difference in compatibility

// Lodash
_.takeRight([1, 2, 3], 0);
// => []

// Native
[1, 2, 3].slice(0);
// => [1, 2, 3]

Browser Support for Array.prototype.slice()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.0 ✔1.0 ✔

⬆ back to top

_.isArray

Returns true if given value is an array.

// Lodash
var array = []
console.log(_.isArray(array))
// output: true

// Native
var array = []
console.log(Array.isArray(array));
// output: true

Browser Support for Array.isArray()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
5.0 ✔4.0 ✔9.0 ✔10.5 ✔5.0 ✔

⬆ back to top

_.isArrayBuffer

Checks if value is classified as an ArrayBuffer object.

// Lodash
_.isArrayBuffer(new ArrayBuffer(2));
// output: true

// Native
console.log(new ArrayBuffer(2) instanceof ArrayBuffer);
// output: true

:warning: You will get the wrong result if you get ArrayBuffer from another realm. See details.

Browser Support for instanceof

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.0 ✔

⬆ back to top

_.join

:heavy_exclamation_mark:Not in Underscore.js Joins a list of elements in an array with a given separator.

// Lodash
var result = _.join(['one', 'two', 'three'], '--')
console.log(result)
// output: 'one--two--three'

// Native
var result = ['one', 'two', 'three'].join('--')
console.log(result)
// output: 'one--two--three'

Browser Support for Array.prototype.join()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.0 ✔1.0 ✔5.5 ✔

⬆ back to top

_.last

Returns the last element of an array. Passing n will return the last n elements of the array.

// Underscore/Lodash
const numbers = [1, 2, 3, 4, 5];
_.last(numbers);
// => 5

// Underscore
_.last(numbers, 2);
// => [4, 5]

// Native
const numbers = [1, 2, 3, 4, 5];
numbers[numbers.length - 1];
// => 5
//or
numbers.slice(-1)[0];
// => 5
//or
[].concat(numbers).pop()
// => 5

// Native (works even with potentially undefined/null)
[].concat(undefined).pop()
// => undefined

numbers.slice(-2);
// => [4, 5]

Browser Support for Array.prototype.concat()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.0 ✔1.0 ✔5.5 ✔

⬆ back to top

_.lastIndexOf

Returns the index of the last occurrence of value in the array, or -1 if value is not present.

// Underscore/Lodash
var array = [2, 9, 9, 4, 3, 6]
var result = _.lastIndexOf(array, 9)
console.log(result)
// output: 2

// Native
var array = [2, 9, 9, 4, 3, 6]
var result = array.lastIndexOf(9)
console.log(result)
// output: 2

Browser Support for Array.prototype.lastIndexOf()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.5 ✔9.0 ✔

⬆ back to top

_.reverse

:heavy_exclamation_mark:Not in Underscore.js Reverses array so that the first element becomes the last, the second element becomes the second to last, and so on.

// Lodash
var array = [1, 2, 3]
console.log(_.reverse(array))
// output: [3, 2, 1]

// Native
var array = [1, 2, 3]
console.log(array.reverse())
// output: [3, 2, 1]

Voice from the Lodash author:

Lodash's _.reverse just calls Array#reverse and enables composition like _.map(arrays, _.reverse). It's exposed on _ because previously, like Underscore, it was only exposed in the chaining syntax. --- jdalton

Browser Support for Array.prototype.reverse()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.5 ✔9 ✔

⬆ back to top

_.slice

Returns a shallow copy of a portion of an array into a new array object selected from begin to end (end not included)

// Lodash
var array = [1, 2, 3, 4]
console.log(_.slice(array, 1, 3))
// output: [2, 3]

// Native
var array = [1, 2, 3, 4]
console.log(array.slice(1, 3));
// output: [2, 3]

Browser Support for Array.prototype.slice()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.0 ✔1.0 ✔

⬆ back to top

_.without

:heavy_exclamation_mark:Not in Underscore.js Returns an array where matching items are filtered.

// Lodash
var array = [1, 2, 3]
console.log(_.without(array, 2))
// output: [1, 3]

// Native
var array = [1, 2, 3]
console.log(array.filter(function(value) {
  return value !== 2;
}));
// output: [1, 3]

Browser Support for Array.prototype.filter()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.0 ✔1.5 ✔9 ✔

⬆ back to top

_.initial

Returns everything but the last entry of the array. Pass n to exclude the last n elements from the result.

// Underscore
var array = [5, 4, 3, 2, 1]
console.log(_.initial(array, 2))
// output: [5, 4, 3]

// Native
var array = [5, 4, 3, 2, 1]
console.log(array.slice(0, -2));
// output: [5, 4, 3]

Browser Support for Array.prototype.slice()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.0 ✔1.0 ✔

⬆ back to top

_.pull

Removes all provided values from the given array using strict equality for comparisons, i.e. ===.

// Lodash
const array = [1, 2, 3, 1, 2, 3];
_.pull(array, 2, 3);
console.log(array); // output: [1, 1]
// Native JS
const array = [1, 2, 3, 1, 2, 3];
function pull(arr, ...removeList){
    var removeSet = new Set(removeList)
    return arr.filter(function(el){
        return !removeSet.has(el)
    })
}
console.log(pull(array, 2, 3)); // output: [1, 1]
console.log(array); // still [1, 2, 3, 1, 2, 3]. This is not in place, unlike lodash!
// TypeScript
const array = [1, 2, 3, 1, 2, 3];
const pull = <T extends unknown>(sourceArray: T[], ...removeList: T[]): T[] => {
  const removeSet = new Set(removeList);
  return sourceArray.filter(el => !removeSet.has(el));
};
console.log(pull(array, 2, 3)); // output: [1, 1]
console.log(array); // still [1, 2, 3, 1, 2, 3]. This is not in place, unlike lodash!

Browser Support for Array.prototype.filter()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.0 ✔1.5 ✔9 ✔

Browser Support for Set.prototype.has()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
38 ✔12 ✔13 ✔11 ✔25 ✔8 ✔

⬆ back to top

_.unionBy

Creates an array of unique values, taking an iteratee to compute uniqueness with (note that to iterate by a key in an object you must use x => x.key instead of key for the iteratee)

// Lodash
var array1 = [2.1];
var array2 = [1.2, 2.3];
var result = _.unionBy(array1, array2, Math.floor)
console.log(result)
// output: [2.1, 1.2]

// Native
var array1 = [2.1];
var array2 = [1.2, 2.3];
function unionBy(...arrays) {
    const iteratee = (arrays).pop();

    if (Array.isArray(iteratee)) {
        return []; // return empty if iteratee is missing
    }

    return [...arrays].flat().filter(
        (set => (o) => set.has(iteratee(o)) ? false : set.add(iteratee(o)))(new Set()),
    );
};
console.log(unionBy(array1, array2, Math.floor))
// output: [2.1, 1.2]

Browser Support for Array.prototype.flat()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
69 ✔62 ✔56 ✔12 ✔

Browser Support for Array.isArray()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
5.0 ✔4.0 ✔9.0 ✔10.5 ✔5.0 ✔

Browser Support for Set.prototype.has()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
38 ✔12 ✔13 ✔11 ✔25 ✔8 ✔

⬆ back to top

Collection*

:heavy_exclamation_mark:Important: Note that most native equivalents are array methods, and will not work with objects. If this functionality is needed and no object method is provided, then Lodash/Underscore is the better option.

_.each

Iterates over a list of elements, yielding each in turn to an iteratee function.

// Underscore/Lodash
//For arrays
_.each([1, 2, 3], function (value, index) {
  console.log(value)
})
// output: 1 2 3

//For objects
_.each({'one':1, 'two':2, 'three':3}, function(value) {
  console.log(value)
})
// output: 1 2 3

// Native
//For arrays
[1, 2, 3].forEach(function (value, index) {
  console.log(value)
})
// output: 1 2 3

//For objects
Object.entries({'one':1, 'two':2, 'three':3}).forEach(function([key,value],index) {
  console.log(value)
})
//output: 1 2 3

Browser Support for Array.prototype.forEach()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.5 ✔9.0 ✔

Browser Support for Object.entries().forEach()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
54 ✔14 ✔47 ✔41 ✔10.1✔

⬆ back to top

_.every

Tests whether all elements in the array pass the test implemented by the provided function.

// Underscore/Lodash
function isLargerThanTen (element, index, array) {
  return element >= 10
}
var array = [10, 20, 30]
var result = _.every(array, isLargerThanTen)
console.log(result)
// output: true

// Native
function isLargerThanTen (element, index, array) {
  return element >= 10
}

var array = [10, 20, 30]
var result = array.every(isLargerThanTen)
console.log(result)
// output: true

Browser Support fpr Array.prototype.every()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.5 ✔9.0 ✔

⬆ back to top

_.filter

Creates a new array with all elements that pass the test implemented by the provided function.

// Underscore/Lodash
function isBigEnough (value) {
  return value >= 10
}
var array = [12, 5, 8, 130, 44]
var filtered = _.filter(array, isBigEnough)
console.log(filtered)
// output: [12, 130, 44]

// Native
function isBigEnough (value) {
  return value >= 10
}
var array = [12, 5, 8, 130, 44]
var filtered = array.filter(isBigEnough)
console.log(filtered)
// output: [12, 130, 44]

Browser Support for Array.prototype.filter()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.5 ✔9 ✔

⬆ back to top

_.groupBy

Group items by key.

// Underscore/Lodash
var grouped = _.groupBy(['one', 'two', 'three'], 'length')
console.log(grouped)
// output: {3: ["one", "two"], 5: ["three"]}

// Native
var grouped = ['one', 'two', 'three'].reduce((r, v, i, a, k = v.length) => ((r[k] || (r[k] = [])).push(v), r), {})
console.log(grouped)
// output: {3: ["one", "two"], 5: ["three"]}
// Underscore/Lodash
var grouped = _.groupBy([1.3, 2.1, 2.4], num => Math.floor(num))
console.log(grouped)
// output: {1: [1.3], 2: [2.1, 2.4]}

// Native
var grouped = [1.3, 2.1, 2.4].reduce((r, v, i, a, k = Math.floor(v)) => ((r[k] || (r[k] = [])).push(v), r), {})
console.log(grouped)
// output: {1: [1.3], 2: [2.1, 2.4]}

Browser Support for Array.prototype.reduce()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
3.0 ✔9.0 ✔10.5 ✔4.0 ✔

⬆ back to top

_.includes

Checks if a value is in collection.

var array = [1, 2, 3]
// Underscore/Lodash - also called _.contains
_.includes(array, 1)
// output: true

// Native
var array = [1, 2, 3]
array.includes(1)
// output: true

// Native (does not use same value zero)
var array = [1, 2, 3]
array.indexOf(1) > -1
// output: true

Browser Support for Array.prototype.includes

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
47.0 ✔14.0 ✔43.0 ✔34.0 ✔9.0 ✔

Browser Support for Array.prototype.indexOf

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.5 ✔9.0 ✔

⬆ back to top

_.keyBy

:heavy_exclamation_mark: Not in Underscore.js Creates an object composed of keys generated from the results of running each element of collection through iteratee.

// Lodash
console.log(_.keyBy(['a', 'b', 'c']))
// output: { a: 'a', b: 'b', c: 'c' }
console.log(_.keyBy([{ id: 'a1', title: 'abc' }, { id: 'b2', title: 'def' }], 'id'))
// output: { a1: { id: 'a1', title: 'abc' }, b2: { id: 'b2', title: 'def' } }
console.log(_.keyBy({ data: { id: 'a1', title: 'abc' }}, 'id'))
// output: { a1: { id: 'a1', title: 'abc' }}

// keyBy for array only
const keyBy = (array, key) => (array || []).reduce((r, x) => ({ ...r, [key ? x[key] : x]: x }), {});

// Native
console.log(keyBy(['a', 'b', 'c']))
// output: { a: 'a', b: 'b', c: 'c' }
console.log(keyBy([{ id: 'a1', title: 'abc' }, { id: 'b2', title: 'def' }], 'id'))
// output: { a1: { id: 'a1', title: 'abc' }, b2: { id: 'b2', title: 'def' } }
console.log(keyBy(Object.values({ data: { id: 'a1', title: 'abc' }}), 'id'))
// output: { a1: { id: 'a1', title: 'abc' }}

// keyBy for array and object
const collectionKeyBy = (collection, key) => {
  const c = collection || {};
  return c.isArray() ? keyBy(c, key) : Object.values(keyBy(c, key));
}

Browser Support for Array.prototype.reduce()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
12.0 ✔3.0 ✔9.0 ✔10.5 ✔4.0 ✔

⬆ back to top

_.map

Translates all items in an array or object to new array of items.

// Underscore/Lodash
var array1 = [1, 2, 3]
var array2 = _.map(array1, function (value, index) {
  return value * 2
})
console.log(array2)
// output: [2, 4, 6]

// Native
var array1 = [1, 2, 3]
var array2 = array1.map(function (value, index) {
  return value * 2
})
console.log(array2)
// output: [2, 4, 6]
// Underscore/Lodash
var object1 = { 'a': 1, 'b': 2, 'c': 3 }
var object2 = _.map(object1, function (value, index) {
  return value * 2
})
console.log(object2)
// output: [2, 4, 6]

// Native
var object1 = { 'a': 1, 'b': 2, 'c': 3 }
var object2 = Object.entries(object1).map(function ([key, value], index) {
  return value * 2
})
console.log(object2)
// output: [2, 4, 6]

Browser Support for Object.entries() and destructuring

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.5 ✔

Browser Support for Array.prototype.map()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.5 ✔9.0 ✔

⬆ back to top

_.minBy and _.maxBy

Use Array#reduce for find the maximum or minimum collection item

// Underscore/Lodash
var data = [{ value: 6 }, { value: 2 }, { value: 4 }]
var minItem = _.minBy(data, 'value')
var maxItem = _.maxBy(data, 'value')
console.log(minItem, maxItem)
// output: { value: 2 } { value: 6 }

// Native
var data = [{ value: 6 }, { value: 2 }, { value: 4 }]
var minItem = data.reduce(function(a, b) { return a.value <= b.value ? a : b }, {})
var maxItem = data.reduce(function(a, b) { return a.value >= b.value ? a : b }, {})
console.log(minItem, maxItem)
// output: { value: 2 }, { value: 6 }

Extract a functor and use es2015 for better code

// utils
const makeSelect = (comparator) => (a, b) => comparator(a, b) ? a : b
const minByValue = makeSelect((a, b) => a.value <= b.value)
const maxByValue = makeSelect((a, b) => a.value >= b.value)

// main logic
const data = [{ value: 6 }, { value: 2 }, { value: 4 }]
const minItem = data.reduce(minByValue, {})
const maxItem = data.reduce(maxByValue, {})

console.log(minItem, maxItem)
// output: { value: 2 }, { value: 6 }

// or also more universal and little slower variant of minBy
const minBy = (collection, key) => {
  // slower because need to create a lambda function for each call...
  const select = (a, b) => a[key] <= b[key] ? a : b
  return collection.reduce(select, {})
}

console.log(minBy(data, 'value'))
// output: { value: 2 }

Browser Support for Array.prototype.reduce()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
3.0 ✔9.0 ✔10.5 ✔4.0 ✔

⬆ back to top

_.pluck

array.map or _.map can also be used to replace _.pluck. Lodash v4.0 removed _.pluck in favor of _.map with iteratee shorthand. Details can be found in Changelog

// Underscore/Lodash
var array1 = [{name: "Alice"}, {name: "Bob"}, {name: "Jeremy"}]
var names = _.pluck(array1, "name")
console.log(names)
// output: ["Alice", "Bob", "Jeremy"]

// Native
var array1 = [{name: "Alice"}, {name: "Bob"}, {name: "Jeremy"}]
var names = array1.map(function(x){
  return x.name
})
console.log(names)
// output: ["Alice", "Bob", "Jeremy"]

Browser Support for Array.prototype.map()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.5 ✔9.0 ✔

⬆ back to top

_.reduce

Applies a function against an accumulator and each value of the array (from left-to-right) to reduce it to a single value.

// Underscore/Lodash
var array = [0, 1, 2, 3, 4]
var result = _.reduce(array, function (previousValue, currentValue, currentIndex, array) {
  return previousValue + currentValue
})
console.log(result)
// output: 10

// Native
var array = [0, 1, 2, 3, 4]
var result = array.reduce(function (previousValue, currentValue, currentIndex, array) {
  return previousValue + currentValue
})
console.log(result)
// output: 10

Browser Support for Array.prototype.reduce()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
3.0 ✔9.0 ✔10.5 ✔4.0 ✔

⬆ back to top

_.range

Creates an array of numbers progressing from start up to.

// Underscore/Lodash
_.range(4)  // output: [0, 1, 2, 3]
_.range(-4) // output: [0, -1, -2, -3]
_.range(1, 5)     // output: [1, 2, 3, 4]
_.range(0, 20, 5) // output: [0, 5, 10, 15]

// Native ( solution with Array.from )
Array.from({length: 4}, (_, i) => i)  // output: [0, 1, 2, 3]
Array.from({length: 4}, (_, i) => -i) // output: [-0, -1, -2, -3]
Array.from({length: 4}, (_, i) => i + 1) // output: [1, 2, 3, 4]
Array.from({length: 4}, (_, i) => i * 5) // output: [0, 5, 10, 15]

// Native ( solution with keys() and spread )
[...Array(4).keys()]  // output: [0, 1, 2, 3]
[...Array(4).keys()].map(k => -k) // output: [-0, -1, -2, -3]
[...Array(4).keys()].map(k => k + 1)  // output: [1, 2, 3, 4]
[...Array(4).keys()].map(k => k * 5)  // output: [0, 5, 10, 15]

Browser Support for Array.from()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
45.0 ✔32.0 ✔9.0 ✔

Browser Support for keys and spread in Array literals

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
46.0 ✔12.0 ✔16.0 ✔37.0 ✔7.1 ✔

⬆ back to top

_.reduceRight

This method is like _.reduce except that it iterates over elements of collection from right to left.

// Underscore/Lodash
var array = [0, 1, 2, 3, 4]
var result = _.reduceRight(array, function (previousValue, currentValue, currentIndex, array) {
  return previousValue - currentValue
})
console.log(result)
// output: -2

// Native
var array = [0, 1, 2, 3, 4]
var result = array.reduceRight(function (previousValue, currentValue, currentIndex, array) {
  return previousValue - currentValue
})
console.log(result)
// output: -2

Browser Support for Array.prototype.reduceRight()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
3.0 ✔9.0 ✔10.5 ✔4.0 ✔

⬆ back to top

_.reject

The opposite of _.filter; this method returns the elements of collection that predicate does not return truthy for.

// Underscore/Lodash
var array = [1, 2, 3, 4, 5];
var result = _.reject(array, function (x) {
  return x % 2 === 0;
});
// output: [1, 3, 5]

// Native
var array = [1, 2, 3, 4, 5];

var reject = function (arr, predicate) {
  var complement = function (f) {
    return function (x) {
      return !f(x);
    }
  };

  return arr.filter(complement(predicate));
};
// output: [1, 3, 5]

Browser Support for Array.prototype.filter()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
12 ✔1.5 ✔9.0 ✔9.5 ✔3.0 ✔

⬆ back to top

_.sample

Gets a random element from array.

// Underscore/Lodash
const array = [0, 1, 2, 3, 4]
const result = _.sample(array)
console.log(result)
// output: 2

// Native
const array = [0, 1, 2, 3, 4]
const sample = arr => {
  const len = arr == null ? 0 : arr.length
  return len ? arr[Math.floor(Math.random() * len)] : undefined
}

const result = sample(array)
console.log(result)
// output: 2

Browser Support for Array.prototype.length() and Math.random()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.0 ✔

⬆ back to top

_.size

Returns the number of values in the collection.

// Underscore/Lodash
var result = _.size({one: 1, two: 2, three: 3})
console.log(result)
// output: 3

// Native
var result2 = Object.keys({one: 1, two: 2, three: 3}).length
console.log(result2)
// output: 3

Browser Support for Object.keys()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
5.0 ✔4.0 ✔9.0 ✔12.0 ✔5.0 ✔

⬆ back to top

_.some

Tests whether any of the elements in the array pass the test implemented by the provided function.

// Underscore/Lodash
function isLargerThanTen (element, index, array) {
  return element >= 10
}
var array = [10, 9, 8]
var result = _.some(array, isLargerThanTen)
console.log(result)
// output: true

// Native
function isLargerThanTen (element, index, array) {
  return element >= 10
}

var array = [10, 9, 8]
var result = array.some(isLargerThanTen)
console.log(result)
// output: true

Browser Support for Array.prototype.some()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.5 ✔✔ 9.0

⬆ back to top

_.sortBy and _.orderBy

Sorts an array of object based on an object key provided by a parameter (note this is more limited than Underscore/Lodash).

const fruits = [
  {name:"banana", amount: 2},
  {name:"apple", amount: 4},
  {name:"pineapple", amount: 2},
  {name:"mango", amount: 1}
];

// Underscore
_.sortBy(fruits, 'name');
// => [{name:"apple", amount: 4}, {name:"banana", amount: 2}, {name:"mango", amount: 1}, {name:"pineapple", amount: 2}]

// Lodash
_.orderBy(fruits, ['name'],['asc']);
// => [{name:"apple", amount: 4}, {name:"banana", amount: 2}, {name:"mango", amount: 1}, {name:"pineapple", amount: 2}]

// Native
const sortBy = (key) => {
  return (a, b) => (a[key] > b[key]) ? 1 : ((b[key] > a[key]) ? -1 : 0);
};

// The native sort modifies the array in place. `_.orderBy` and `_.sortBy` do not, so we use `.concat()` to
// copy the array, then sort.
fruits.concat().sort(sortBy("name"));
// => [{name:"apple", amount: 4}, {name:"banana", amount: 2}, {name:"mango", amount: 1}, {name:"pineapple", amount: 2}]

Browser Support for Array.prototype.concat() and Array.prototype.sort()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.0 ✔1.0 ✔5.5 ✔

⬆ back to top

_.uniq

Produces a duplicate-free version of the array, using === to test object equality.

// Underscore/Lodash
var array = [1, 2, 1, 4, 1, 3]
var result = _.uniq(array)
console.log(result)
// output: [1, 2, 4, 3]

// Native
var array = [1, 2, 1, 4, 1, 3];
var result = [...new Set(array)];
console.log(result)
// output: [1, 2, 4, 3]

Browser Support for Spread in array literals

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
46.0 ✔12.0 ✔16.0 ✔37.0 ✔8.0 ✔

⬆ back to top

Function

_.after

:heavy_exclamation_mark:Note this is an alternative implementation Creates a version of the function that will only be run after first being called count times. Useful for grouping asynchronous responses, where you want to be sure that all the async calls have finished, before proceeding.

var notes = ['profile', 'settings']
// Underscore/Lodash
var renderNotes = _.after(notes.length, render)
notes.forEach(function (note) {
  console.log(note)
  renderNotes()
})

// Native
notes.forEach(function (note, index) {
  console.log(note)
  if (notes.length === (index + 1)) {
    render()
  }
})

Browser Support for Array.prototype.forEach()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.5 ✔9.0 ✔

⬆ back to top

_.bind

Create a new function that calls func with thisArg and args.

var objA = {
  x: 66,
  offsetX: function(offset) {
    return this.x + offset;
  }
}

var objB = {
  x: 67
};

// Underscore/Lodash
var boundOffsetX = _.bind(objA.offsetX, objB, 0);

// Native
var boundOffsetX = objA.offsetX.bind(objB, 0);

Browser Support for Function.prototype.bind()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
7.0 ✔4.0 ✔9.0 ✔11.6 ✔5.1 ✔

⬆ back to top

_.isFunction

Checks if value is classified as a Function object.

// Lodash
_.isFunction(console.log);
// => true

_.isFunction(/abc/);
// => false

// Native
function isFunction(func) {
  return typeof func === "function";
}

isFunction(setTimeout);
// => true

isFunction(123);
// => false

Browser Support

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]

⬆ back to top

_.debounce

Create a new function that calls func with thisArg and args.

 function debounce(func, wait, immediate) {
  var timeout;
  return function() {
      var context = this, args = arguments;
      clearTimeout(timeout);
      timeout = setTimeout(function() {
          timeout = null;
          if (!immediate) func.apply(context, args);
      }, wait);
      if (immediate && !timeout) func.apply(context, args);
  };
}

// Avoid costly calculations while the window size is in flux.
jQuery(window).on('resize', debounce(calculateLayout, 150));

Browser Support for debounce

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
7.0 ✔4.0 ✔9.0 ✔11.6 ✔5.1 ✔

⬆ back to top

_.partial

Create a new function that calls func with args.

// Lodash
function greet(greeting, name) {
  return greeting + ' ' + name;
}
var sayHelloTo = _.partial(greet, 'Hello');
var result = sayHelloTo('Jose')
console.log(result)
// output: 'Hello Jose'

// Native
function greet(greeting, name) {
  return greeting + ' ' + name;
}
var sayHelloTo = (...args) => greet('Hello', ...args)
var result = sayHelloTo('Jose')
console.log(result)
// output: 'Hello Jose'

// Native
const partial = (func, ...boundArgs) => (...remainingArgs) => func(...boundArgs, ...remainingArgs)
var sayHelloTo = partial(greet, 'Hello');
var result = sayHelloTo('Jose')
console.log(result)
// output: 'Hello Jose'

Browser Support for Spread

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
46.0 ✔12.0 ✔16.0 ✔37.0 ✔8.0 ✔

⬆ back to top

_.throttle

Create a new function that limits calls to func to once every given timeframe.

function throttle(func, timeFrame) {
  var lastTime = 0;
  return function (...args) {
      var now = new Date();
      if (now - lastTime >= timeFrame) {
          func(...args);
          lastTime = now;
      }
  };
}

// Avoid running the same function twice within the specified timeframe.
jQuery(window).on('resize', throttle(calculateLayout, 150));

Browser Support for throttle

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
5.0 ✔12.0 ✔3.0 ✔9.0 ✔10.5 ✔4.0 ✔

⬆ back to top

Lang

_.castArray

Puts the value into an array of length one if it is not already an array.

// Underscore
console.log(_.castArray(5))
// output: [5]
console.log(_.castArray([2]))
// output: [2]

// Native
function castArray(arr) {
  return Array.isArray(arr) ? arr : [arr]
}
// output: true
console.log(castArray(5));
// output: [5]
console.log(castArray([2]));
// output: [2]

Browser Support for Array.isArray()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
5.0 ✔4.0 ✔9.0 ✔10.5 ✔5.0 ✔

⬆ back to top

_.isDate

Checks if value is classified as a Date object.

// Lodash
console.log(_.isDate(new Date));
// output: true

// Native
console.log(Object.prototype.toString.call(new Date) === "[object Date]");
// output: true

Browser Support for String.prototype.toString.call()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]

⬆ back to top

_.gt

Checks if value is greater than other.

// Lodash
console.log(_.gt(3, 1))
// output: true

// Native
console.log(3 > 1);
// output: true

Browser Support

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]

⬆ back to top

_.gte

Checks if value is greater than or equal to other.

// Lodash
console.log(_.gte(3, 1))
// output: true

// Native
console.log(3 >= 1);
// output: true

Browser Support

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]

⬆ back to top

_.isEmpty

Checks if value is an empty object or collection.

:heavy_exclamation_mark:Note that the Native version does not support evaluating a Set or a Map

// Lodash
console.log(_.isEmpty(null))
// output: true
console.log(_.isEmpty(''))
// output: true
console.log(_.isEmpty({}))
// output: true
console.log(_.isEmpty([]))
// output: true
console.log(_.isEmpty({a: '1'}))
// output: false

// Native
const isEmpty = obj => [Object, Array].includes((obj || {}).constructor) && !Object.entries((obj || {})).length;

console.log(isEmpty(null))
// output: true
console.log(isEmpty(''))
// output: true
console.log(isEmpty({}))
// output: true
console.log(isEmpty([]))
// output: true
console.log(isEmpty({a: '1'}))
// output: false

Browser Support for Array.prototype.includes()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
47.0 ✔14.0 ✔43.0 ✔34.0 ✔9.0 ✔

⬆ back to top

_.isFinite

Checks if value is a finite primitive number.

// Lodash
console.log(_.isFinite('3'))
// output: false
console.log(_.isFinite(3))
// output: true

// Native
console.log(Number.isFinite('3'))
// output: false
console.log(Number.isFinite(3))
// output: true

Browser Support for Number.isFinite()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
19.0 ✔16.0 ✔15.0 ✔9.0 ✔

⬆ back to top

_.isInteger

Checks if value is an integer.

// Lodash
console.log(_.isInteger(3))
// output: true
console.log(_.isInteger('3'))
// output: false

// Native
console.log(Number.isInteger(3))
// output: true
console.log(Number.isInteger('3'))
// output: false

Browser Support for Number.isInteger()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
12 ✔16.0 ✔

⬆ back to top

_.isNaN

Checks if a value is NaN.

// Underscore/Lodash
console.log(_.isNaN(NaN))
// output: true

// Native
console.log(isNaN(NaN))
// output: true

// ES6
console.log(Number.isNaN(NaN))
// output: true

MDN:

In comparison to the global isNaN() function, Number.isNaN() doesn't suffer the problem of forcefully converting the parameter to a number. This means it is now safe to pass values that would normally convert to NaN, but aren't actually the same value as NaN. This also means that only values of the type number, that are also NaN, return true. Number.isNaN()

Voice from the Lodash author:

Lodash's _.isNaN is equiv to ES6 Number.isNaN which is different than the global isNaN. --- jdalton

Browser Support for isNaN

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.0 ✔

Browser Support for Number.isNaN

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
25.0 ✔15.0 ✔9.0 ✔

⬆ back to top

_.isNil

:heavy_exclamation_mark:Not in Underscore.js Checks if value is null or undefined.

// Lodash
console.log(_.isNil(null))
// output: true
console.log(_.isNil(NaN))
// output: false
console.log(_.isNil(undefined))
// output: true

// Native
console.log(null == null);
// output: true
console.log(NaN == null);
// output: false
console.log(undefined == null)
// output: true

Browser Support

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]

⬆ back to top

_.isNull

Checks if value is null or undefined.

// Underscore/Lodash
console.log(_.isNull(null))
// output: true
console.log(_.isNull(void 0))
// output: false

// Native
console.log(null === null);
// output: true
console.log(void 0 === null);
// output: false

Browser Support

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]

⬆ back to top

_.isUndefined

Checks if value is undefined.

// Underscore/Lodash
console.log(_.isUndefined(a))
// output: true

// Native
console.log(typeof a === 'undefined');
// output: true
console.log(a === undefined);
// output: true

Browser Support

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1 ✔

⬆ back to top

Object

_.assign

The method is used to copy the values of all enumerable own properties from one or more source objects to a target object.

// Underscore: _.extendOwn
// Lodash
function Foo() {
  this.c = 3;
}
function Bar() {
  this.e = 5;
}
Foo.prototype.d = 4;
Bar.prototype.f = 6;
var result = _.assign(new Foo, new Bar);
console.log(result);
// output: { 'c': 3, 'e': 5 }

// Native
function Foo() {
  this.c = 3;
}
function Bar() {
  this.e = 5;
}
Foo.prototype.d = 4;
Bar.prototype.f = 6;
var result = Object.assign({}, new Foo, new Bar);
console.log(result);
// output: { 'c': 3, 'e': 5 }

Browser Support for Object.assign()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
45.0 ✔34.0 ✔32.0 ✔9.0 ✔

⬆ back to top

_.defaults

The method is used to apply new values over an object with default values for keys.

// Underscore: _.defaults
// Lodash
const newValues = {a: 3};
const defaultValues = {a: 1, b: 2}
const appliedValues = _.defaults(newValues, defaultValues);
console.log(appliedValues)
// output { a: 3, b: 2 }

// Native
const newValues = {a: 3};
const defaultValues = {a: 1, b: 2}
const appliedValues = Object.assign({}, defaultValues, newValues);
// output { a: 3, b: 2 }

Browser Support for Object.assign()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
45.0 ✔34.0 ✔32.0 ✔9.0 ✔

⬆ back to top

_.extend

The method is used to copy the values of all enumerable own and inherited properties from one or more source objects to a target object.

// Underscore
// Lodash: _.assignIn
function Foo() {
  this.c = 3;
}
function Bar() {
  this.e = 5;
}
Foo.prototype.d = 4;
Bar.prototype.f = 6;
var result = _.extend({}, new Foo, new Bar);
console.log(result);
// output: { 'c': 3, 'd': 4, 'e': 5, 'f': 6 }

// Native
function Foo() {
  this.c = 3;
}
function Bar() {
  this.e = 5;
}
Foo.prototype.d = 4;
Bar.prototype.f = 6;
var result = Object.assign({}, new Foo, Foo.prototype, new Bar, Bar.prototype);
console.log(result);
// output: { 'c': 3, 'd': 4, 'e': 5, 'f': 6 }

//Or using a function
const extend = (target, ...sources) => {
 const length = sources.length;

  if (length < 1 || target == null) return target;
  for (let i = 0; i < length; i++) {
    const source = sources[i];

    for (const key in source) {
      target[key] = source[key];
    }
  }
  return target;
};
console.log(extend({}, new Foo, new Bar));
// output: { 'c': 3, 'd': 4, 'e': 5, 'f': 6 }

Browser Support for Object.assign()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
45.0 ✔34.0 ✔32.0 ✔9.0 ✔

⬆ back to top

_.has

Checks if key is a direct property of object. Key may be a path of a value separated by .

// Lodash
var object = { a: 1, b: 'settings', c: { d: 'test' } };

var hasA = _.has(object, 'a');
var hasCWhichHasD = _.has(object, 'c.d')

console.log(hasA);
// output: true
console.log(hasCWhichHasD);
// output: true

// Native
const has = function (obj, key) {
  var keyParts = key.split('.');

  return !!obj && (
    keyParts.length > 1
      ? has(obj[key.split('.')[0]], keyParts.slice(1).join('.'))
      : hasOwnProperty.call(obj, key)
  );
};

var object = { a: 1, b: 'settings' };
var result = has(object, 'a');
// output: true

Browser Support for Object .hasOwnProperty

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
12 ✔5.5 ✔5 ✔3 ✔

⬆ back to top

_.get

Gets the value at path of object. Note: If provided path does not exist inside the object js will generate error.

// Lodash
var object = { a: [{ b: { c: 3 } }] };
var result = _.get(object, 'a[0].b.c', 1);
console.log(result);
// output: 3

// Native (ES6 - IE not supported)
var object = { a: [{ b: { c: 3 } }] };
var { a: [{ b: { c: result = 1 } = {} } = {}] = [] } = object;
console.log(result);
// output: 3

// Native (ES11)
var object = { a: [{ b: { c: 3 } }] };
var result = object?.a?.[0]?.b?.c ?? 1;
console.log(result);
// output: 3

// Native
const get = (obj, path, defaultValue = undefined) => {
  const travel = regexp =>
    String.prototype.split
      .call(path, regexp)
      .filter(Boolean)
      .reduce((res, key) => (res !== null && res !== undefined ? res[key] : res), obj);
  const result = travel(/[,[\]]+?/) || travel(/[,[\].]+?/);
  return result === undefined || result === obj ? defaultValue : result;
};

var object = { a: [{ b: { c: 3 } }] };
var result = get(object, 'a[0].b.c', 1);
// output: 3

Browser Support for Object destructing

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
49.0 ✔14.0 ✔41.0 ✔41.0 ✔8.0 ✔

Browser Support for optional chaining ?.

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
80.0 ✔80.0 ✔74.0 ✔67.0 ✔13.1 ✔

Browser Support for nullish coalescing operator ??

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
80.0 ✔80.0 ✔72.0 ✔13.1 ✔

⬆ back to top

_.keys

Retrieves all the names of the object's own enumerable properties.

// Underscore/Lodash
var result = _.keys({one: 1, two: 2, three: 3})
console.log(result)
// output: ["one", "two", "three"]

// Native
var result2 = Object.keys({one: 1, two: 2, three: 3})
console.log(result2)
// output: ["one", "two", "three"]

Browser Support for Object.keys()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
5.0 ✔4.0 ✔9.0 ✔12.0 ✔5.0 ✔

⬆ back to top

_.omit

Returns a copy of the object, filtered to omit the keys specified.

var object = { 'a': 1, 'b': '2', 'c': 3 };

// Underscore/Lodash
var result = _.omit(object, ['a', 'c']);
console.log(result)
// output: { 'b': '2' }

// Native
var { a, c, ...result2 } = object;
console.log(result2)
// output: { 'b': '2' }

Browser Support for Spread in object literals

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
60.0 ✔55.0 ✔37.0 ✔

⬆ back to top

_.pick

Creates an object composed of the object properties predicate returns truthy for.

var object = { 'a': 1, 'b': '2', 'c': 3 };

// Underscore/Lodash
var result = _.pick(object, ['a', 'c']);
console.log(result)
// output: {a: 1, c: 3}

// Native
const { a, c } = object;
const result = { a, c};
console.log(result);
// output: {a: 1, c: 3}
// for an array of this object --> array.map(({a, c}) => ({a, c}));

// Native
function pick(object, keys) {
  return keys.reduce((obj, key) => {
     if (object && object.hasOwnProperty(key)) {
        obj[key] = object[key];
     }
     return obj;
   }, {});
}
var result = pick(object, ['a', 'c']);
console.log(result)
// output: {a: 1, c: 3}

Browser Support

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
38.0 ✔13.0 ✔12.0 ✔25.0 ✔7.1 ✔

⬆ back to top

_.pickBy

Creates an object composed of the object properties predicate returns truthy for.

var object = { 'a': 1, 'b': null, 'c': 3, 'd': false, 'e': undefined };

// Underscore/Lodash
var result = _.pickBy(object);
console.log(result)
// output: {a: 1, c: 3}

// Native
function pickBy(object) {
    const obj = {};
    for (const key in object) {
        if (object[key]) {
            obj[key] = object[key];
        }
    }
    return obj;
}
var result = pickBy(object);
console.log(result)
// output: {a: 1, c: 3}

Browser Support

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
6.0 ✔

⬆ back to top

_.toPairs

Retrieves all the given object's own enumerable property [ key, value ] pairs.

// Underscore - also called _.pairs
// Lodash - also called _.entries
var result = _.toPairs({one: 1, two: 2, three: 3})
console.log(result)
// output: [["one", 1], ["two", 2], ["three", 3]]

// Native
var result2 = Object.entries({one: 1, two: 2, three: 3})
console.log(result2)
// output: [["one", 1], ["two", 2], ["three", 3]]

Browser Support for Object.entries()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
54.0 ✔14.0 ✔47.0 ✔41.0 ✔10.1 ✔

⬆ back to top

_.values

Retrieves all the given object's own enumerable property values.

// Underscore/Lodash
var result = _.values({one: 1, two: 2, three: 3})
console.log(result)
// output: [1, 2, 3]

// Native
var result2 = Object.values({one: 1, two: 2, three: 3})
console.log(result2)
// output: [1, 2, 3]

Browser Support for Object.values()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
54.0 ✔14.0 ✔47.0 ✔41.0 ✔10.1 ✔

⬆ back to top

String

_.capitalize

:heavy_exclamation_mark:Not in Underscore.js Converts the first character of string to upper case and the remaining to lower case.

// Lodash
var result = _.capitalize('FRED');
console.log(result);
// => 'Fred'

// Native
const capitalize = (string) => {
  return string ? string.charAt(0).toUpperCase() + string.slice(1).toLowerCase() : '';
};

var result = capitalize('FRED');
console.log(result);
// => 'Fred'

Browser Support

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]

_.endsWith

:heavy_exclamation_mark:Not in Underscore.js Checks if string ends with the given target string.

// Lodash
_.endsWith('abc', 'c');
// => true

_.endsWith('abc', 'b');
// => false

_.endsWith('abc', 'b', 2);
// => true

// Native
'abc'.endsWith('c');
// => true

'abc'.endsWith('b');
// => false

'abc'.endsWith('b', 2);
// => true

Browser Support for String.prototype.endsWith()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
41.0 ✔17.0 ✔28.0 ✔9.0 ✔

_.isString

Checks if value is classified as a String primitive or object.

// Lodash
_.isString('abc');
// => true

_.isString(123);
// => false

// Native
function isString(str){
  if (str != null && typeof str.valueOf() === "string") {
    return true
  }
  return false
}

isString('abc');
// => true

isString(123);
// => false

Browser Support

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]

_.lowerFirst

:heavy_exclamation_mark:Not in Underscore.js Converts the first character of string to lower case.

// Lodash
var result = _.lowerFirst('Fred')
console.log(result)
// output: 'fred'

// Native
const lowerFirst = (string) => {
  return string ? string.charAt(0).toLowerCase() + string.slice(1) : ''
}

var result = lowerFirst('Fred')
console.log(result)
// output: 'fred'
![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]

_.padStart and _.padEnd

:heavy_exclamation_mark:Not in Underscore.js Pads the current string with another string (multiple times, if needed) until the resulting string reaches the given length.

// Lodash
console.log(_.padStart('123', 5, '0'))
// output: '00123'

console.log(_.padEnd('123', 5, '0'))
// output: '12300'

// Native
console.log('123'.padStart(5, '0'))
// output: '00123'

console.log('123'.padEnd(5, '0'))
// output: '12300'

Browser Support for String.prototype.padStart() and String.prototype.padEnd()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
57.0 ✔15.0 ✔48.0 ✔44.0 ✔10.0 ✔

_.repeat

:heavy_exclamation_mark:Not in Underscore.js Repeats the given string n times.

// Lodash
var result = _.repeat('abc', 2)
console.log(result)
// output: 'abcabc'

// Native
var result = 'abc'.repeat(2)
console.log(result)
// output: 'abcabc'

Browser Support for String.prototype.repeat()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
41.0 ✔24.0 ✔28.0 ✔9.0 ✔

_.replace

returns a new string with some or all matches of a pattern replaced by a replacement

// Lodash
var re = /apples/gi;
var str = 'Apples are round, and apples are juicy.';
var newstr = _.replace(str, re, 'oranges');
console.log(newstr);
// output: 'oranges are round, and oranges are juicy.'

// Native
var re = /apples/gi;
var str = 'Apples are round, and apples are juicy.';
var result = str.replace(re, 'oranges');
console.log(result);
// output: 'oranges are round, and oranges are juicy.'

Browser Support for String.prototype.replace()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.0 ✔

_.split

:heavy_exclamation_mark:Not in Underscore.js Splits string by separator.

// Lodash
var result = _.split('a-b-c', '-', 2)
  console.log(result)
// output: ['a','b']

// Native
var result = 'a-b-c'.split('-', 2)
console.log(result)
// output: ['a','b']

Browser Support for String.prototype.split()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.0 ✔

_.startsWith

:heavy_exclamation_mark:Not in Underscore.js Checks if string starts with the given target string.

// Lodash
var result = _.startsWith('abc', 'b', 1)
console.log(result)
// output: true

// Native
var result = 'abc'.startsWith('b', 1)
console.log(result)
// output: true

Browser Support for String.prototype.startsWith()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
41.0 ✔17.0 ✔28.0 ✔9.0 ✔

_.template

:heavy_exclamation_mark: Note this is an alternative implementation. Native template literals not escape html.

Create a template function.

// Lodash/Underscore
const compiled = _.template('hello <%= user %>!');
var result = compiled({ 'user': 'fred' });
console.log(result);
// output: 'hello fred'

// Native
const templateLiteral = (value) => `hello ${value.user}`;
var result = templateLiteral({ 'user': 'fred' });
console.log(result);
// output: 'hello fred'

Browser Support for String (template) literals

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
41.0 ✔12.0 ✔34.0 ✔28.0 ✔9.0 ✔

_.toLower

:heavy_exclamation_mark:Not in Underscore.js Lowercases a given string.

// Lodash
var result = _.toLower('FOOBAR')
console.log(result)
// output: 'foobar'

// Native
var result = 'FOOBAR'.toLowerCase()
console.log(result)
// output: 'foobar'

Browser Support for String.prototype.toLowerCase()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.0 ✔

_.toUpper

:heavy_exclamation_mark:Not in Underscore.js Uppercases a given string.

// Lodash
var result = _.toUpper('foobar')
console.log(result)
// output: 'FOOBAR'

// Native
var result = 'foobar'.toUpperCase()
console.log(result)
// output: 'FOOBAR'

Browser Support for String.prototype.toUpperCase()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
1.0 ✔

_.trim

:heavy_exclamation_mark:Not in Underscore.js Removes the leading and trailing whitespace characters from a string.

// Lodash
var result = _.trim(' abc ')
console.log(result)
// output: 'abc'

// Native
var result = ' abc '.trim()
console.log(result)
// output: 'abc'

Browser Support for String.prototype.trim()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
5.0 ✔3.5 ✔9.0 ✔10.5 ✔5.0 ✔

_.upperFirst

:heavy_exclamation_mark:Not in Underscore.js Uppercases the first letter of a given string

// Lodash
var result = _.upperFirst('george')
console.log(result)
// output: 'George'

// Native
const upperFirst = (string) => {
  return string ? string.charAt(0).toUpperCase() + string.slice(1) : ''
}

var result = upperFirst('george')
console.log(result)
// output: 'George'
![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]

Reference

_.uniqWith

similar to _.uniq except that it accepts comparator which is invoked to compare elements of array. The order of result values is determined by the order they occur in the array.

// Lodash
const objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 2 }];
const result = _.uniqWith(objects, _.isEqual);
console.log(result);
// output: [{ x: 1, y: 2 }, { x: 2, y: 1 }]

// Native
const uniqWith = (arr, fn) => arr.filter((element, index) => arr.findIndex((step) => fn(element, step)) === index);

const array = [1, 2, 2, 3, 4, 5, 2];
const result = uniqWith(array, (a, b) => a === b);
console.log(result);
// output: [1, 2, 3, 4, 5]

const objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 2 }];
const result = uniqWith(objects, (a, b) => JSON.stringify(a) === JSON.stringify(b));
console.log(result);
// output: [{ x: 1, y: 2 }, { x: 2, y: 1 }]

Browser Support for Array.prototype.filter() and Array.prototype.findIndex()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
45.0 ✔12.0 ✔25.0 ✔32.0 ✔8.0 ✔

Util

_.times

Invokes the iteratee n times, returning an array of the results of each invocation.

// Lodash
var result = _.times(10)
console.log(result)
// output: '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]'

// Native
var result = Array.from({length: 10}, (_,x) => x)
console.log(result)
// output: '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]'

// Native
var result = [...Array(10).keys()]
console.log(result)
// output: '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]'

Browser Support for Array.from()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]
45.0 ✔32.0 ✔9.0 ✔

Number

_.clamp

Clamps number within the inclusive lower and upper bounds.

// Lodash
_.clamp(-10, -5, 5);
// => -5

_.clamp(10, -5, 5);
// => 5

_.clamp(10, -5);
// => -5

_.clamp(10, 99);
// => 10

// Native
const clamp = (number, boundOne, boundTwo) => {
  if (!boundTwo) {
    return Math.max(number, boundOne) === boundOne ? number : boundOne;
  } else if (Math.min(number, boundOne) === number) {
    return boundOne;
  } else if (Math.max(number, boundTwo) === number) {
    return boundTwo;
  }
  return number;
};

clamp(-10, -5, 5);
// => -5

clamp(10, -5, 5);
// => 5

clamp(10, -5);
// => -5

clamp(10, 99);
// => 10

Browser Support for Math.min() and Math.max()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]

_.inRange

Checks if n is between start and up to, but not including, end. If end is not specified, it's set to start with start then set to 0. If start is greater than end the params are swapped to support negative ranges.

  // Lodash
  _.inRange(3, 2, 4);
  // output: true
  _.inRange(-3, -2, -6);
  // output: true

  //Native
  const inRange = (num, init, final) => {
    if(final === undefined){
      final = init;
      init = 0;
    }
    return (num >= Math.min(init, final) && num < Math.max(init, final));
  }

  //Native
  const inRange = (num, a, b=0) => (Math.min(a,b) <= num && num < Math.max(a,b));

  inRange(3, 2, 4);
  // output: true
  inRange(-3, -2, -6);
  // output: true

Browser Support for Math.min() and Math.max()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]

_.random

Produces a random number between the inclusive lower and upper bounds. If only one argument is provided a number between 0 and the given number is returned. If floating is true, or either lower or upper are floats, a floating-point number is returned instead of an integer.

  // Lodash
  _.random(0, 5);
  // => an integer between 0 and 5

  _.random(5);
  // => also an integer between 0 and 5

  _.random(5, true);
  // => a floating-point number between 0 and 5

  _.random(1.2, 5.2);
  // => a floating-point number between 1.2 and 5.2

  //Native ES6
  const random = (a = 1, b = 0) => {
    const lower = Math.min(a, b);
    const upper = Math.max(a, b);
    return lower + Math.random() * (upper - lower);
  };

  const randomInt = (a = 1, b = 0) => {
    const lower = Math.ceil(Math.min(a, b));
    const upper = Math.floor(Math.max(a, b));
    return Math.floor(lower + Math.random() * (upper - lower + 1))
  };

  random();
  // => a floating-point number between 0 and 1

  random(5);
  // => a floating-point number between 0 and 5

  random(0, 5);
  // => also a floating-point number between 0 and 5

  random(1.2, 5.2);
  // => a floating-point number between 1.2 and 5.2

  randomInt();
  // => just 0 or 1

  randomInt(5);
  // => an integer between 0 and 5

  randomInt(0, 5);
  // => also an integer between 0 and 5

  randomInt(1.2, 5.2);
  // => an integer between 2 and 5

Browser Support for Math.random()

![Chrome][chrome-image]![Edge][edge-image]![Firefox][firefox-image]![IE][ie-image]![Opera][opera-image]![Safari][safari-image]

Inspired by:

Author: You-dont-need
Source Code: https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore 
License: MIT license

#javascript #utilities #native 

List Of JavaScript Methods Which You Can Use Natively + ESLint Plugin
Hunter  Krajcik

Hunter Krajcik

1658734080

Native_ios_dialog: A Flutter Plugin Which Makes It Straightforward

Native iOS Dialog

A Flutter plugin which makes it straightforward to show the native equivalent of a CupertinoAlertDialog or CupertinoActionSheet dialog.

Usage

To use this plugin, add native_ios_dialog as a dependency in your pubspec.yaml file.

Dialogs itself in Flutter are pretty awesome. However, the CupertinoAlertDialogs do not provide the same feeling as native iOS dialogs, so I created this plugin. With this plugin, you have all the customization options iOS provides.

Types of dialogs

  • Alert
  • Action Sheet

Types of buttons

  • Default
  • Destructive
  • Cancel

Each button can also be disabled

Sample Usage

Info dialog

NativeIosDialog(title: "Info", message: "Please consider the following information in this dialog.", style: style, actions: [
  NativeIosDialogButton(text: "OK", style: NativeIosDialogButtonStyle.defaultStyle, onPressed: () {}),
]).show();

Confirm dialog

NativeIosDialog(title: "Confirm", message: "Please confirm the following information in this dialog.", style: style, actions: [
  NativeIosDialogButton(text: "OK", style: NativeIosDialogButtonStyle.defaultStyle, onPressed: () {}),
  NativeIosDialogButton(text: "Cancel", style: NativeIosDialogButtonStyle.cancel, onPressed: () {}),
]).show();

Installing

Use this package as a library

Depend on it

Run this command:

With Flutter:

 $ flutter pub add native_ios_dialog

This will add a line like this to your package's pubspec.yaml (and run an implicit flutter pub get):

dependencies:
  native_ios_dialog: ^0.0.2

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

Import it

Now in your Dart code, you can use:

import 'package:native_ios_dialog/native_ios_dialog.dart';

example/lib/main.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

import 'package:native_ios_dialog/native_ios_dialog.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const CupertinoApp(
      debugShowCheckedModeBanner: false,
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  int currentDialogStyle = 0;

  @override
  Widget build(BuildContext context) {
    final NativeIosDialogStyle style = currentDialogStyle == 0
        ? NativeIosDialogStyle.alert
        : NativeIosDialogStyle.actionSheet;
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text("Native iOS Dialogs"),
      ),
      child: ListView(
        children: [
          const SizedBox(height: 12),
          const SimpleDivider(text: "Basic examples"),
          SimpleButton(
            text: "Info Dialog",
            onPressed: () {
              NativeIosDialog(
                  title: "Info",
                  message:
                      "Please consider the following information in this dialog.",
                  style: style,
                  actions: [
                    NativeIosDialogButton(
                        text: "OK",
                        style: NativeIosDialogButtonStyle.defaultStyle,
                        onPressed: () {}),
                  ]).show();
            },
          ),
          SimpleButton(
            text: "Confirm Dialog",
            onPressed: () {
              NativeIosDialog(
                  title: "Confirm",
                  message:
                      "Please confirm the following information in this dialog.",
                  style: style,
                  actions: [
                    NativeIosDialogButton(
                        text: "OK",
                        style: NativeIosDialogButtonStyle.defaultStyle,
                        onPressed: () {}),
                    NativeIosDialogButton(
                        text: "Cancel",
                        style: NativeIosDialogButtonStyle.cancel,
                        onPressed: () {}),
                  ]).show();
            },
          ),
          SimpleButton(
            text: "Destructive Confirm Dialog",
            onPressed: () {
              NativeIosDialog(
                  title: "Confirm deletion",
                  message:
                      "Do you want to delete this resource? You cannot undo this action.",
                  style: style,
                  actions: [
                    NativeIosDialogButton(
                        text: "Delete",
                        style: NativeIosDialogButtonStyle.destructive,
                        onPressed: () {}),
                    NativeIosDialogButton(
                        text: "Cancel",
                        style: NativeIosDialogButtonStyle.cancel,
                        onPressed: () {}),
                  ]).show();
            },
          ),
          SimpleButton(
            text: "Three Buttons",
            onPressed: () {
              NativeIosDialog(
                  title: "Are you sure?",
                  message:
                      "Do you want to apply the changes you made, or do you want to delete this resource?",
                  style: style,
                  actions: [
                    NativeIosDialogButton(
                        text: "Apply",
                        style: NativeIosDialogButtonStyle.defaultStyle,
                        onPressed: () {}),
                    NativeIosDialogButton(
                        text: "Delete",
                        style: NativeIosDialogButtonStyle.destructive,
                        onPressed: () {}),
                    NativeIosDialogButton(
                        text: "Cancel",
                        style: NativeIosDialogButtonStyle.cancel,
                        onPressed: () {}),
                  ]).show();
            },
          ),
          const SimpleDivider(text: "Examples with disabled actions"),
          SimpleButton(
            text: "Confirm Dialog",
            onPressed: () {
              NativeIosDialog(
                  title: "Confirm",
                  message:
                      "Please confirm the following information in this dialog.\n\nOh wait!\nSeems like there is something that prohibits you from confirming this dialog.",
                  style: style,
                  actions: [
                    NativeIosDialogButton(
                        text: "OK",
                        style: NativeIosDialogButtonStyle.defaultStyle),
                    NativeIosDialogButton(
                        text: "Cancel",
                        style: NativeIosDialogButtonStyle.cancel,
                        onPressed: () {}),
                  ]).show();
            },
          ),
          SimpleButton(
            text: "Destructive Confirm Dialog",
            onPressed: () {
              NativeIosDialog(
                  title: "Confirm deletion",
                  message:
                      "Do you want to delete this resource? You cannot undo this action.\n\nOh wait!\nSeems like there is something that prohibits you from confirming this dialog.",
                  style: style,
                  actions: [
                    NativeIosDialogButton(
                        text: "Delete",
                        style: NativeIosDialogButtonStyle.destructive),
                    NativeIosDialogButton(
                        text: "Cancel",
                        style: NativeIosDialogButtonStyle.cancel,
                        onPressed: () {}),
                  ]).show();
            },
          ),
          SimpleButton(
            text: "Three Buttons",
            onPressed: () {
              NativeIosDialog(
                  title: "Are you sure?",
                  message:
                      "Do you want to apply the changes you made, or do you want to delete this resource?",
                  style: style,
                  actions: [
                    NativeIosDialogButton(
                        text: "Apply",
                        style: NativeIosDialogButtonStyle.defaultStyle),
                    NativeIosDialogButton(
                        text: "Delete",
                        style: NativeIosDialogButtonStyle.destructive),
                    NativeIosDialogButton(
                        text: "Cancel",
                        style: NativeIosDialogButtonStyle.cancel,
                        onPressed: () {}),
                  ]).show();
            },
          ),
          const SimpleDivider(text: "Other"),
          SimpleButton(
            text: "100 Buttons",
            onPressed: () {
              NativeIosDialog(
                  title: "Uhm...",
                  message:
                      "Please do not use this in production... please do not do this! This dialog is just a demonstration of how iOS handles many buttons.",
                  style: style,
                  actions: [
                    for (int i = 1; i <= 100; i++)
                      NativeIosDialogButton(
                          text: "Action $i",
                          style: NativeIosDialogButtonStyle.defaultStyle,
                          onPressed: () {}),
                    NativeIosDialogButton(
                        text: "Cancel",
                        style: NativeIosDialogButtonStyle.cancel,
                        onPressed: () {}),
                  ]).show();
            },
          ),
          const SimpleDivider(text: "Configuration"),
          Padding(
            padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 8.0),
            child: CupertinoSlidingSegmentedControl(
                groupValue: currentDialogStyle,
                children: const {0: Text("Alert"), 1: Text("ActionSheet")},
                onValueChanged: (int? newValue) {
                  if (newValue == null) return;
                  setState(() {
                    currentDialogStyle = newValue;
                  });
                }),
          )
        ],
      ),
    );
  }
}

class SimpleButton extends StatelessWidget {
  final String text;
  final VoidCallback onPressed;

  const SimpleButton({Key? key, required this.text, required this.onPressed})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8),
      child: CupertinoButton(
        child: Text(
          text,
          style: const TextStyle(color: Colors.white),
        ),
        color: Theme.of(context).primaryColor,
        onPressed: onPressed,
      ),
    );
  }
}

class SimpleDivider extends StatelessWidget {
  final String text;

  const SimpleDivider({Key? key, required this.text}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(top: 8.0, left: 16, right: 16),
      child:
          Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
        const Expanded(
            child: Divider(
          color: Colors.black,
        )),
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Text(text),
        ),
        const Expanded(
            child: Divider(
          color: Colors.black,
        )),
      ]),
    );
  }
}

Author: Gebes
Source Code: https://github.com/Gebes/native_ios_dialog 
License: MIT license

#flutter #dart #ios #native 

Native_ios_dialog: A Flutter Plugin Which Makes It Straightforward
Hunter  Krajcik

Hunter Krajcik

1658459280

A Dart Binding to Native Compression Libraries, including Lz4@1.9.3.

dart_native_compression

LZ4

The lz4 in this library is a ffi binding to lz4 v1.9.3, the compressed block and frame format are both interoperable with official C api, as well as other interoperable ports, bindings and CLI tools.

Getting Started

import 'dart:ffi';
import 'package:dart_native_compression/dart_native_compression.dart';

final lz4 = Lz4Lib(lib: DynamicLibrary.open('libnative_compression.so'));
print('LZ4 version number: ${lz4.getVersioinNumber()}');

To compress data into a lz4 frame

final compressedFrame = lz4.compressFrame(data);

To decompress a lz4 frame with a single function

final decompressed = lz4.decompressFrame(compressedFrame);

To decompress a lz4 frame with stream api

await for (final decompressedChunk
    in lz4.decompressFrameStream(compressedStream)) {
    // Your logic here
}

To get more examples

Go to unit test

To run unit tests

pub get && pub run test

To build native lib (libnative_compression.so)

install rust

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

pull submodules

git submodule update --init --recursive

go to native_compression directory, run

cargo build --release --all-features

to cross compile to different target

cargo lipo --release --targets=aarch64-apple-ios,x86_64-apple-ios,armv7-apple-ios,armv7s-apple-ios
cargo build --target aarch64-linux-android --release
cargo build --target armv7-linux-androideabi --release
cargo build --target i686-linux-android --release

The shared library will be under target/release . It would be libdart_native_compression.so, dart_native_compression.dll, libdart_native_compression.dylib on linux, windows, osx respectively.

Installing

Use this package as a library

Depend on it

Run this command:

With Dart:

 $ dart pub add dart_native_compression

With Flutter:

 $ flutter pub add dart_native_compression

This will add a line like this to your package's pubspec.yaml (and run an implicit dart pub get):

dependencies:
  dart_native_compression: ^1.1.0+4

Alternatively, your editor might support dart pub get or flutter pub get. Check the docs for your editor to learn more.

Import it

Now in your Dart code, you can use:

import 'package:dart_native_compression/dart_native_compression.dart';

example/example.md

Getting Started

import 'dart:ffi';
import 'package:dart_native_compression/dart_native_compression.dart';

final lz4 = Lz4Lib(lib: DynamicLibrary.open('libnative_compression.so'));
print('LZ4 version number: ${lz4.getVersioinNumber()}');

To compress data into a lz4 frame

final compressedFrame = lz4.compressFrame(data);

To decompress a lz4 frame with a single function

final decompressed = lz4.decompressFrame(compressedFrame);

To decompress a lz4 frame with stream api

await for (final decompressedChunk
    in lz4.decompressFrameStream(compressedStream)) {
    // Your logic here
}

To get more examples

Go to unit test

Author: Hanabi1224
Source Code: https://github.com/hanabi1224/flutter_native_extensions 
License: MIT license

#flutter #dart #native 

A Dart Binding to Native Compression Libraries, including Lz4@1.9.3.
Royce  Reinger

Royce Reinger

1657148400

Bloomfilter-rb: BloomFilter(s) in Ruby

BloomFilter(s) in Ruby

  • Native (MRI/C) counting bloom filter
  • Redis-backed getbit/setbit non-counting bloom filter
  • Redis-backed set-based counting (+TTL) bloom filter

Bloom filter is a space-efficient probabilistic data structure that is used to test whether an element is a member of a set. False positives are possible, but false negatives are not. For more detail, check the wikipedia article. Instead of using k different hash functions, this implementation seeds the CRC32 hash with k different initial values (0, 1, ..., k-1). This may or may not give you a good distribution, it all depends on the data.

Performance of the Bloom filter depends on a number of variables:

  • size of the bit array
  • size of the counter bucket
  • number of hash functions

MRI/C API Example

MRI/C implementation which creates an in-memory filter which can be saved and reloaded from disk.

require 'bloomfilter-rb'

bf = BloomFilter::Native.new(:size => 100, :hashes => 2, :seed => 1, :bucket => 3, :raise => false)
bf.insert("test")
bf.include?("test")     # => true
bf.include?("blah")     # => false

bf.delete("test")
bf.include?("test")     # => false

# Hash with a bloom filter!
bf["test2"] = "bar"
bf["test2"]             # => true
bf["test3"]             # => false

bf.stats
# => Number of filter bits (m): 10
# => Number of filter elements (n): 2
# => Number of filter hashes (k) : 2
# => Predicted false positive rate = 10.87%

Redis-backed setbit/getbit bloom filter

Uses getbit/setbit on Redis strings - efficient, fast, can be shared by multiple/concurrent processes.

bf = BloomFilter::Redis.new

bf.insert('test')
bf.include?('test')     # => true
bf.include?('blah')     # => false

bf.delete('test')
bf.include?('test')     # => false

Memory footprint

  • 1.0% error rate for 1M items, 10 bits/item: 2.5 mb
  • 1.0% error rate for 150M items, 10 bits per item: 358.52 mb
  • 0.1% error rate for 150M items, 15 bits per item: 537.33 mb

Redis-backed counting bloom filter with TTLs

Uses regular Redis get/set counters to implement a counting filter with optional TTL expiry. Because each "bit" requires its own key in Redis, you do incur a much larger memory overhead.

bf = BloomFilter::CountingRedis.new(:ttl => 2)

bf.insert('test')
bf.include?('test')     # => true

sleep(2)
bf.include?('test')     # => false

Resources


Credits

Tatsuya Mori valdzone@gmail.com (Original C implementation: http://vald.x0.com/sb/)

Author: igrigorik
Source Code: https://github.com/igrigorik/bloomfilter-rb 
License: MIT License

#ruby #c #redis #native 

Bloomfilter-rb: BloomFilter(s) in Ruby
Mike  Kozey

Mike Kozey

1656784920

Windows Implementation Of The Flutter_exprtk Plugin

flutter_exprtk_native

The Windows, Android, iOS and macOS implementation of flutter_exprtk.

Usage

Import the package

This package has been endorsed, meaning that you only need to add flutter_exprtk as a dependency in your pubspec.yaml. It will be automatically included in your app when you depend on package:flutter_exprtk.

This is what the above means to your pubspec.yaml:

...
dependencies:
  ...
  flutter_exprtk: ^0.0.7
  ...

If you wish to use the package without web support, you can add flutter_exprtk_native as a dependency:

...
dependencies:
  ...
  flutter_exprtk_native: ^0.0.7
  ...

Installing

Use this package as a library

Depend on it

Run this command:

With Flutter:

 $ flutter pub add flutter_exprtk_native

This will add a line like this to your package's pubspec.yaml (and run an implicit flutter pub get):

dependencies:
  flutter_exprtk_native: ^0.0.7

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

Import it

Now in your Dart code, you can use:

import 'package:flutter_exprtk_native/flutter_exprtk_native.dart';

Original article source at: https://pub.dev/packages/flutter_exprtk_native 

#flutter #dart #native #windows 

Windows Implementation Of The Flutter_exprtk Plugin
Walker  Orn

Walker Orn

1654941840

Learn Typescript and Native ESM in Node.js

Typescript and Native ESM in Node.js How to Work on A Npm Package That Can Be Published As an Esm Package with Typescript. We Walk You Through Tsconfig And Package.json Settings.

#typescript #native #esm #nodejs 

Learn Typescript and Native ESM in Node.js

Cómo Convertir Su Aplicación Angular En Una Aplicación Móvil Nativa

¿Está buscando una manera de convertir su aplicación Angular existente en una aplicación móvil sin tener que volver a escribir todo desde cero? Bueno, has venido al lugar indicado. En este artículo, usaremos Capacitor para lograr eso.

requisitos previos

Antes de comenzar, asegúrese de configurar su entorno instalando las siguientes dependencias:

¿Qué es el condensador?

Capacitor es un tiempo de ejecución nativo multiplataforma para crear aplicaciones web modernas que pueden ejecutarse de forma nativa en iOS o Android con facilidad. Ofrece características geniales como compatibilidad con PWA, Capacitor CLI y complementos increíbles.

Consulte los documentos para obtener más información .

 

1 — Generar una nueva aplicación angular

En aras de la simplicidad, solo convertiremos el proyecto angular generado desde la CLI.

ng new angular-mobile-app

El comando CLI anterior creará e inicializará una nueva aplicación Angular lista para usar.

Cuando finalice la generación, navegue hasta el directorio del proyecto y escriba el siguiente comando para iniciar el servidor de desarrollo:

cd angular-mobile-app
ng serve

Ahora, vaya a su navegador y escriba http://localhost:4200 , debería estar viendo la página a continuación si todo salió bien.

Aplicación angular que se ejecuta en localhost: 4200

2 — Agregue un condensador a su proyecto

Ahora que tenemos una aplicación web en funcionamiento, el siguiente paso es instalar ambas dependencias ejecutando @capacitor/corelos @capacitor/clisiguientes comandos npm:

cd angular-mobile-app
npm install @capacitor/core
npm install @capacitor/cli

3 — Configure el archivo de configuración del capacitor

Después de instalar las dependencias del capacitor, ejecute el siguiente comando para generar el capacitor.config.tsarchivo:

npx cap init

La CLI le hará algunas preguntas generales sobre la aplicación. Solo deja todo como predeterminado por ahora.

El archivo generado se verá así:

import { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  appId: 'com.example.app',
  appName: 'angular-mobile-app',
  webDir: 'dist/angular-mobile-app',
  bundledWebRuntime: false
};

export default config;

capacitor.config.ts

  • appId: El identificador único de su aplicación empaquetada.
  • appName: el nombre de su aplicación.
  • webDir: El directorio de sus activos web compilados.
  • bundledWebRuntime: Si copiar o no el paquete de tiempo de ejecución de Capacitor.

Nota: asegúrese de cambiar el valor de webDira dist/(your app name)ya que esa es la ruta de compilación habitual para una aplicación Angular. Ahora, capacitor podrá encontrar los activos web.

4 — Agregue paquetes nativos de iOS y Android

El siguiente paso es instalar los paquetes de acuerdo con iOS y Android y luego agregar los proyectos nativos ejecutando los siguientes comandos:

npm install @capacitor/ios @capacitor/androidnpx cap add iosnpx cap add android

Después de ejecutar los comandos CLI anteriores, debería ver dos nuevas carpetas creadas para Android e iOS.

5 — Cree su aplicación

Ahora construyamos nuestra aplicación Angular y sincronicemos los cambios a los proyectos nativos de Android/iOS ejecutando los siguientes comandos:

ng build --prodnpx cap sync

npx cap synccopiará la carpeta de compilación en el proyecto iOS/Android.

6 — Abre el estudio de Android o Xcode

Puede abrir directamente el proyecto móvil en Android Studio o Xcode con el siguiente comando en su proyecto:

npx cap open ios
npx cap open android

Nota: asegúrese de configurar su entorno primero antes de ejecutar los comandos anteriores.

Ahora, el siguiente paso es usar Android Studio o Xcode para implementar su aplicación directamente en un dispositivo conectado o en un dispositivo virtual.

En mi caso, estoy usando Android Studio. Como puede ver en la imagen a continuación, estoy seleccionando un emulador de Pixel 4.

estudio android

Finalmente, haga clic en el botón ejecutar o depurar para ejecutar su aplicación.

7 — ¡Ejecuta la aplicación!

aplicación que se ejecuta en un dispositivo Android virtual

Terminando

Como puede ver, Capacitor es muy poderoso para convertir una aplicación angular existente en una aplicación móvil nativa.

Tener una base de código para múltiples plataformas es la última habilidad que se debe tener en estos días para que no solo ahorremos tiempo y dinero, sino que también aumentemos la productividad. ¡Disfrutar!

¡Gracias por leer y nos vemos en la próxima!

Fuente: https://betterprogramming.pub/how-to-convert-your-angular-application-to-a-native-mobile-app-android-and-ios-c212b38976df 

#angular #android #native #mobileapp #ios 

Cómo Convertir Su Aplicación Angular En Una Aplicación Móvil Nativa
高橋  花子

高橋 花子

1653616800

Angularアプリケーションをネイティブモバイルアプリに変換する

すべてを最初から書き直すことなく、既存のAngularアプリケーションをモバイルアプリに変換する方法をお探しですか?さて、あなたは正しい場所に来ました。この記事では、それを実現するためにコンデンサを使用します。

前提条件

開始する前に、以下の依存関係をインストールして環境を設定してください。

コンデンサとは何ですか?

Capacitorは、iOSまたはAndroidで簡単にネイティブに実行できる最新のWebアプリを構築するためのクロスプラットフォームのネイティブランタイムです。PWAサポート、コンデンサCLI、すばらしいプラグインなどの優れた機能を提供します。

詳細については、ドキュメントを確認してください

 

1 —新しいAngularアプリケーションを生成します

簡単にするために、CLIから生成されたAngularプロジェクトを変換するだけです。

ng new angular-mobile-app

上記のCLIコマンドは、箱から出して新しいAngularアプリケーションを作成して初期化します。

生成が終了したら、プロジェクトディレクトリに移動し、次のコマンドを入力して開発サーバーを起動します。

cd angular-mobile-app
ng serve

ここで、ブラウザに移動してhttp:// localhost:4200と入力します。すべてがうまくいった場合は、以下のページが表示されます。

localhost:4200で実行されているAngularアプリ

2 —プロジェクトにコンデンサを追加します

Webアプリが起動して実行されたので、次のステップは、次のnpmコマンドを実行して、依存関係@capacitor/coreと依存関係の両方をインストールすることです。@capacitor/cli

cd angular-mobile-app
npm install @capacitor/core
npm install @capacitor/cli

3 —コンデンサ設定ファイルを設定します

コンデンサの依存関係をインストールした後、次のコマンドを実行してcapacitor.config.tsファイルを生成します。

npx cap init

CLIは、アプリに関するいくつかの一般的な質問をします。今のところ、すべてをデフォルトのままにしておきます。

生成されたファイルは次のようになります。

import { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  appId: 'com.example.app',
  appName: 'angular-mobile-app',
  webDir: 'dist/angular-mobile-app',
  bundledWebRuntime: false
};

export default config;

コンデンサ.config.ts

  • appId:パッケージ化されたアプリの一意の識別子。
  • appName:アプリの名前。
  • webDir:コンパイルされたWebアセットのディレクトリ。
  • bundledWebRuntime Capacitorランタイムバンドルをコピーするかどうか。

注:これはAngularアプリの通常のビルドパスであるため、必ずwebDirtoの値を変更してください。dist/(your app name)これで、コンデンサはWebアセットを見つけることができるようになります。

4 —ネイティブiOSおよびAndroidパッケージを追加します

次のステップは、iOSおよびAndroidのパッケージに従ってインストールし、以下のコマンドを実行してネイティブプロジェクトを追加することです。

npm install @capacitor/ios @capacitor/androidnpx cap add iosnpx cap add android

上記のCLIコマンドを実行すると、AndroidとiOSの両方用に作成された2つの新しいフォルダーが表示されます。

5 —アプリを作成する

次に、Angularアプリをビルドし、以下のコマンドを実行して、ネイティブのAndroid/iOSプロジェクトへの変更を同期しましょう。

ng build --prodnpx cap sync

npx cap synciOS/Androidプロジェクトのビルドフォルダをコピーします。

6 —AndroidStudioまたはXcodeを開きます

プロジェクトで次のコマンドを使用して、AndroidStudioまたはXcodeでモバイルプロジェクトを直接開くことができます。

npx cap open ios
npx cap open android

注:上記のコマンドを実行する前に、まず環境をセットアップしてください。

Now the next step is to use Android Studio or Xcode to deploy your app directly to either a connected device or a virtual device.

In my case, I’m using Android Studio. As you can see in the image below, I’m selecting a Pixel 4 emulator.

android studio

Finally, click the run or debug button to run your app.

7 — Run the app!

app running on a virtual android device

Wrapping up

As you can see, Capacitor is very powerful at converting an existing angular application to a native mobile app.

Having one codebase for multiple platforms is the ultimate skill to have these days in order for us to not only save time and money but also to boost productivity. Enjoy!

Thanks for reading and see you in the next one!

Source: https://betterprogramming.pub/how-to-convert-your-angular-application-to-a-native-mobile-app-android-and-ios-c212b38976df 

#angular #android #native #mobileapp #ios 

Angularアプリケーションをネイティブモバイルアプリに変換する

Androidのネイティブコードで画像を高速処理

パフォーマンスは、世の中に出回っているほとんどのソフトウェア製品の機能ですが、他の製品よりもパフォーマンスに敏感なプログラムはほとんどありません。私はAndroidカメラアプリに取り組んでおり、私のチームはパフォーマンスを非常に優先しています。

[Googleが見つけた]10件の結果を含むページの生成には0.4秒かかりました。結果が30のページは0.9秒かかりました。0.5秒の遅延により、トラフィックが20%減少しました。0.5秒の遅延により、ユーザーの満足度が低下しました。

performanceマネージャーやあなたにとって重要性を売りすぎる必要はありませんが、パフォーマンスを強化するこのスニペットは機能構成であることに気づきました。事実を知っておくのは良いことです。

カメラでキャプチャした大きな画像やデバイス上の既存の画像を処理するアプリケーションを作成する場合は、特に注意する必要があります。最近の携帯電話のカメラには、高解像度センサーが簡単に搭載されています。現在Androidデバイスに搭載されている13MP(MegaPixels)、24MP、48MP、さらには108MPのカメラを簡単に見つけることができます。

13MPの画像を見てみましょう。13,000,000ピクセルです。画像に対して1つの簡単な計算を実行したい場合は、たとえば、すべてのピクセルに対して画像の露出を増やします。

image(x, y) = std::clamp(alpha * image(x, y) + beta, 0, 255);

あなたはそれを1300万回行う必要があります。

最近のスマートフォンにはマルチコアのSIMD対応CPUも搭載されているため、シリアル化された1,300万回の反復よりも高速に実行する方法がありますが、同時に、実行するアルゴリズムのタイプは通常、私よりもはるかに複雑です。ちょうど述べた。

私の経験では、これらの複雑な画像処理操作をネイティブコードで処理する方が簡単であり、特にパフォーマンスを維持するために優れています。

これは、Androidでネイティブコードを使用して画像処理を行う方法を示す非常に基本的な記事です。また、非常に単純で最適化されていないC ++コードのパフォーマンスが、同じ問題ステートメントに対してかなり最適化されたJavaコードに非常に近いことを例で示します。「AndroidでJavaNativeInterfaceまたはJNIを使​​用した高速画像処理」をお探しの場合—適切な場所に来たと思いますが、この記事は同じように役立ちます。

問題ステートメントの例:YUVからRGBへの変換

問題の説明は、 1つの平面チャネルと2つの半平面サブサンプリングチャネルを持つYUV_420_888と呼ばれる特定の形式の8MP(3264x2448)画像を、Androidのビットマップで一般的にサポートされているARGB_8888形式に変換することです。ウィキペディアでYUVフォーマットの詳細を読むことができます。また、以下の記事には、問題の説明についてのより適切な説明があります。YUV

これを問題ステートメントとして選択した理由はYUV_420_888、Android Camera APIでサポートされている最も一般的なOUTPUT形式の1つであり、画像はBitmapAndroidと同様に一般的に使用されるため、これを対処するためのかなり一般的な問題ステートメントにします。

私は、Androidでの画像処理のパフォーマンスを理解するために、さまざまなフレームワークやテクノロジーのパフォーマンスを実験してきました。これを問題の説明として取り上げています。これが私がテストした他のテクニックを使った同じもののいくつかの例です:

さまざまなアプローチのパフォーマンスを説明する完全なシリーズ記事を書く予定です。これまでに公開されたアプローチの数値は次のとおりです。

8MP (3264x2448)表1: Pixel4aデバイスでYUV画像をビットマップに変換する場合のパフォーマンスレイテンシ

ネイティブコードによる画像処理

ネイティブスペースで直接操作している場合、基本的なスケルトンプログラムは次のようになります。


struct RGBA_8888 {
  uint8_t r;
  uint8_t g;
  uint8_t b;
  uint8_t a = 255;
};

RGBA_8888 yuv2rgb(uint8_t y, uint8_t u, uint8_t v) {
    int r = y + (1.370705 * (v - 128));
    int g = y - (0.698001 * (v - 128)) - (0.337633 * (u - 128));;
    int b = y + (1.732446 * (u - 128));
    r = clamp(r, 0, 255);
    g = clamp(g, 0, 255);
    b = clamp(b, 0, 255);
    return {.r = r, ,g = g, .b = b};
}

この関数は、特定のピクセルのチャネル値を取り、対応する値を返しyます。uvRGBA_8888

AndroidプログラムはデフォルトでJavaまたはKotlin言語で記述されていますが、AndroidツールチェーンにはAndroid NDKと呼ばれるネイティブ開発キットが付属しており、CやC++などの言語を使用してアプリのセクションを実装できます。Androidのドキュメントでは、NDKが次の2つのシナリオで役立つと説明されています。

デバイスから余分なパフォーマンスを引き出して、低遅延を実現したり、ゲームや物理シミュレーションなどの計算量の多いアプリケーションを実行したりします。

自分または他の開発者のCまたはC++ライブラリを再利用します。

次のいくつかのセクションでは、ネイティブコード統合を使用してYUV_420_888形式のJavaイメージをJavaオブジェクトビットマップに変換する方法を簡単に説明します。

したがって、基本的に、このスケルトンJava関数を入力する必要があります。

Bitmap toBitmap(Image image) {
    if (image.getFormat() != ImageFormat.YUV_420_888) {
      throw new IllegalArgumentException("Invalid image format");
    }

    // Do some magic here
    throw new Exception("Not yet implemented");
  }

深く掘り下げる前に、JNIを紹介したいと思います。

JNI

JNIは、JavaNativeInterfaceの略です。これは、JavaまたはKotlinコードからコンパイルされたバイトコードがCまたはC++で記述されたネイティブコードと対話する方法を定義します。名前が示すように、Javaコードをネイティブコードとインターフェースするのに役立ちます。

JNIをさらに活用することになった場合は、 JNIに関するAndroidのヒント—JNIのヒントを読むことをお勧めします。

NDKとJNIの使用を開始する

冗長性を回避するために、次の記事をチェックして試して、最初のJNIベースのAndroidアプリをセットアップすることをお勧めします。これらの記事は、AndroidアプリをNDKおよびネイティブコードとともにコンパイルするために必要なツールチェーンを設定するのにも役立ちます。

これらの記事が十分に明確でない場合は、コメントで知らせてください。JNIアプリを正常に作成して実行した場合、または構成をすでに認識している場合は、次のセクションに進んでください。

YUVからビットマップへの変換用のネイティブコード

にライブラリとソースファイルyuv2rgb.h/ccを作成しますapp/src/main/cpp

ヘッダーファイル


namespace MyProject {

// Converts the YUV image with the given properties to ARGB image and write to
// `argb_output` destination.
void Yuv2Rgb(int width, int height, const uint8_t* y_buffer, const uint8_t* u_buffer,
            const uint8_t* v_buffer, int y_pixel_stride, int uv_pixel_stride,
            int y_row_stride, int uv_row_stride, int* argb_output);

}

ソースファイル

#include "yuv2rgb.h"

namespace MyProject {

void Yuv2Rgb(int width, int height, const uint8_t* y_buffer, const uint8_t* u_buffer,
            const uint8_t* v_buffer, int y_pixel_stride, int uv_pixel_stride,
            int y_row_stride, int uv_row_stride, int* argb_output) {
  uint32_t a = (255u << 24);
  uint8_t r, g, b;
  int16_t y_val, u_val, v_val;

  for (int y = 0; y < height; ++y) {
    for (int x = 0; x < width; ++x) {
      // Y plane should have positive values belonging to [0...255]
      int y_idx = (y * y_row_stride) + (x * y_pixel_stride);
      y_val = static_cast<int16_t>(y_buffer[y_idx]);

      int uvx = x / 2;
      int uvy = y / 2;
      // U/V Values are sub-sampled i.e. each pixel in U/V channel in a
      // YUV_420 image act as chroma value for 4 neighbouring pixels
      int uv_idx = (uvy * uv_row_stride) +  (uvx * uv_pixel_stride);

      u_val = static_cast<int16_t>(u_buffer[uv_idx]) - 128;
      v_val = static_cast<int16_t>(v_buffer[uv_idx]) - 128;

      // Compute RGB values per formula above.
      r = y_val + 1.370705f * v_val;
      g = y_val - (0.698001f * v_val) - (0.337633f * u_val);
      b = y_val + 1.732446f * u_val;

      int argb_idx = y * width + x;
      argb_output[argb_idx] = a | r << 16 | g << 8 | b;
    }
  }
}

}

理想的には、このための単体テストも作成する必要がありますが、それはこの記事の範囲外です。ネイティブテストの追加について詳しくは、Androidのドキュメントをご覧ください。

次に、最初に使用したJavaスケルトンに接続するためのJNIレイヤーを作成します。

Java+JNI統合

次に、Javaライブラリをネイティブライブラリに接続するためのJNIファイルが必要です。yuv2rgb-jni.ccを追加しましょうapp/src/main/cpp

com.example.myprojectまた、Javaスケルトン関数がパッケージ内にあり、という静的クラスの下にあると仮定するとYuvConvertor、JNIファイルは次のようになります。

重要な注意:パッケージ名、クラス名、およびネイティブメソッド名は、ランタイムが適切なネイティブ関数を呼び出すために使用するため、ここでは重要です。詳細なコンテキストを取得するには、以下のJNIコードのメソッド名を参照してください。

#include <jni.h>
#include <android/log.h>

#include "yuv2rgb.h"

extern "C" {

jboolean
Java_com_example_myproject_YuvConvertor_yuv420toArgbNative(
  JNIEnv* env, jclass clazz, jint width, jint height, jobject y_byte_buffer,
    jobject u_byte_buffer, jobject v_byte_buffer, jint y_pixel_stride,
    jint uv_pixel_stride, jint y_row_stride, jint uv_row_stride,
    jintArray argb_array) {
  auto y_buffer = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(y_byte_buffer));
  auto u_buffer = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(u_byte_buffer));
  auto v_buffer = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(v_byte_buffer));
  jint* argb_result_array = env->GetIntArrayElements(argb_array, nullptr);
  if (argb_result_array == nullptr || y_buffer == nullptr || u_buffer == nullptr
      || v_buffer == nullptr) {
    __android_log_print(ANDROID_LOG_ERROR, PRIVATE_TAG,
                        "[yuv420toArgbNative] One or more inputs are null.");
    return false;
  }

  Yuv2Rgb(width, height, reinterpret_cast<const uint8_t*>(y_buffer),
                   reinterpret_cast<const uint8_t*>(u_buffer),
                   reinterpret_cast<const uint8_t*>(v_buffer),
                   y_pixel_stride, uv_pixel_stride, y_row_stride, uv_row_stride,
                   argb_result_array);
  return true;
}

}

そして最後に、これをJavaライブラリから呼び出します

package com.example.myproject;

import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.ImageFormat;
import android.media.Image;

public class YuvConvertor {
  private YuvConvertor() {}

  static {
    // define this in CMakeLists.txt file.
    System.loadLibrary("yuv2rgb-lib");
  }

  public Bitmap toBitmap(Image image) throws Exception {
    if (image.getFormat() != ImageFormat.YUV_420_888) {
      throw new IllegalArgumentException("Invalid image format");
    }

    int[] argbOutput = new int[image.getWidth() * image.getHeight()];
    if (!yuv420toArgbNative(
      image.getWidth(),
      image.getHeight(),
      image.getPlanes()[0].getBuffer(),       // Y buffer
      image.getPlanes()[1].getBuffer(),       // U buffer
      image.getPlanes()[2].getBuffer(),       // V buffer
      image.getPlanes()[0].getPixelStride(),  // Y pixel stride
      image.getPlanes()[1].getPixelStride(),  // U/V pixel stride
      image.getPlanes()[0].getRowStride(),    // Y row stride
      image.getPlanes()[1].getRowStride(),    // U/V row stride
      argbOutput)) {
      // Handle this based on your usecase.
      throw new Exception("Failed to convert YUV to Bitmap");
    }
    return Bitmap.createBitmap(
      argbOutput, image.getWidth(), image.getHeight(), Config.ARGB_8888);
  }

  private static native boolean yuv420toArgbNative(
    int width,
    int height,
    ByteBuffer yByteBuffer,
    ByteBuffer uByteBuffer,
    ByteBuffer vByteBuffer,
    int yPixelStride,
    int uvPixelStride,
    int yRowStride,
    int uvRowStride,
    int[] argbOutput);
}

ここで注意すべきいくつかの重要なポイントは次のとおりです。

これにより、JNIを呼び出すJavaコードが作成されます。これにより、入力データと出力データへのポインターがネイティブ形式で取得され、ネイティブライブラリに渡されて処理されます。ネイティブライブラリはかなり一般的なC++コードであり、Androidの外部でも使用できます。

makeファイル(app/src/main/cpp/CMakeLists.txtこの場合はこの場合)は、AndroidAPKを使用したネイティブコードのビルドをサポートするように適切に構成されている必要があります。この例では、少なくともこれらの定義が必要です。

# Sets the minimum version of CMake required to build the native
# library.
cmake_minimum_required(VERSION 3.4.1)

set(CMAKE_CXX_FLAGS "-Wall -Wextra ${CMAKE_CXX_FLAGS}")

find_library(log-lib
             log )

add_library(
        yuv2rgb-lib
        SHARED
        yuv2rgb-jni.cc
        yuv2rgb.cc)

target_link_libraries( # Specifies the target library.
        yuv2rgb-lib
        -O3
        # Links the target library to the log library included in the NDK.
        ${log-lib} )

include(AndroidNdkModules)

パフォーマンス

パフォーマンスを見るとき、これはC ++コードのかなり単純な形式であり、Androidデバイスで実行できるマルチスレッドまたはSIMD命令セットを明示的に利用していないことに注意してください。せいぜい、コードの一部はコンパイラによって最適化されます(たとえば、メインのforループは自動ベクトル化される可能性があります)。-O3コードは最適化フラグを使用してコンパイルされました。

8MP画像(3264x2448)の場合、このコードは76.4ms同じ参照デバイスで実行されます。

8MP (3264x2448)表2: Pixel4aデバイスでYUV画像をビットマップに変換する場合のパフォーマンスレイテンシ

ここで、このバージョンのネイティブコードのパフォーマンスは、非常に最適化されたマルチスレッドJavaコードよりも1.4倍遅いことがわかります。これは、悪いニュースではありません。スターターJavaコードは353 ms、同じアルゴリズムを実行するために必要でした(ただし、複合体ByteBufferが直接配列アクセスを提供しない理由がある可能性があります-続きを読む)。より最適化されたネイティブコードの記述の詳細について、またはコンパイラを言語構造に利用する方法については、この記事を読むことをお勧めします—C++コンパイラをガイドしてコードを自動ベクトル化する

クロージングノート

ネイティブコードは、Androidで計算量の多いアルゴリズムを実行するのに最適であると再度​​主張します。また、ほとんどの画像処理アルゴリズムは通常、このカテゴリに分類されます。これは、多数のピクセルを処理する必要があるため、したがって多数の反復を処理する必要があるためと考えられます。

この同じ一連の実験では、マルチスレッド、NEON API(SIMD)、およびいくつかのアセンブリコードを利用すること12.1 msで、このシリーズの勝者であるレイテンシーを取得することができました(スポイラーアラート)。ただし、書き込みと保守の両方を行うのはかなり複雑であり、同じ問題ステートメントを処理するHalideベースのソリューション28msは、パフォーマンス、保守、および書き込みの容易さの点で完璧なソリューションですらあります。

読んでくれてありがとう、次の一連の記事でこれらの両方について書きます。乞うご期待!!

ソース:https ://betterprogramming.pub/processing-images-fast-with-native-code-in-android-db8b21001fa9

 #android #code #native 

Androidのネイティブコードで画像を高速処理

Procesamiento Rápido De Imágenes Con Código Nativo En Android

El rendimiento es una característica de la mayoría de los productos de software que existen, pero hay pocos programas que sean más sensibles al rendimiento que los demás. Trabajo en una aplicación de cámara de Android y mi equipo le da una prioridad muy alta al rendimiento.

[Google descubrió que] la página con 10 resultados tardó 0,4 segundos en generarse. La página con 30 resultados tardó 0,9 segundos. Medio segundo de retraso provocó una caída del 20% en el tráfico. Medio segundo de retraso mató la satisfacción del usuario.

No necesito exagerar la importancia de performancemi gerente o de usted, pero me encontré con este fragmento que fortalece el rendimiento es una construcción característica; es bueno saberlo.

Si está escribiendo aplicaciones que procesan imágenes grandes capturadas con una cámara o una imagen existente en el dispositivo, debe tener mucho cuidado. En estos días, las cámaras de los teléfonos se equipan fácilmente con sensores de alta resolución. Es fácil encontrar cámaras de 13 MP (megapíxeles), 24 MP, 48 MP o incluso 108 MP que ahora se envían en dispositivos Android.

Veamos una imagen de 13MP. Tiene 13.000.000 píxeles. Si quisiera hacer solo un cálculo simple en la imagen, digamos aumentar la exposición de la imagen, es decir, para cada píxel

image(x, y) = std::clamp(alpha * image(x, y) + beta, 0, 255);

Necesitas hacerlo 13 millones de veces.

Los teléfonos inteligentes en estos días también están equipados con CPU multinúcleo compatibles con SIMD, por lo que hay formas de hacerlo más rápido que serializar 13 millones de iteraciones pero, al mismo tiempo, los tipos de algoritmos que queremos ejecutar suelen ser mucho más complejos que el que yo recién dicho.

En mi experiencia, es más fácil y mejor manejar estas complejas operaciones de procesamiento de imágenes con código nativo, muy particularmente para mantener su rendimiento.

Este es un artículo muy básico que demuestra cómo procesar imágenes con código nativo en Android. También mostraré con un ejemplo que el rendimiento de un código C++ muy simple y no optimizado se acerca mucho al código Java bastante optimizado para la misma declaración del problema. Si está buscando "Procesamiento rápido de imágenes usando la interfaz nativa de Java o JNI en Android", creo que ha venido al lugar correcto, este artículo lo ayudará de la misma manera.

Ejemplo de declaración del problema: conversión de YUV a RGB

La declaración del problema es convertir una imagen de 8MP (3264x2448) en un formato determinado llamado YUV_420_888 que tiene un Ycanal planar y dos UVcanales submuestreados semiplanares al formato ARGB_8888 que comúnmente es compatible con Bitmap en Android. Puede leer más sobre el formato YUV en Wikipedia . Además, los artículos a continuación tienen una mejor descripción de la declaración del problema.

La razón por la que elegí esto como la declaración del problema es porque YUV_420_888es uno de los formatos de SALIDA más comunes compatibles con las API de la cámara de Android y las imágenes se consumen comúnmente como Bitmapen Android, lo que hace que esta sea una declaración del problema bastante común para abordar.

He estado experimentando con el rendimiento de diferentes marcos o tecnologías para comprender el rendimiento del procesamiento de imágenes en Android tomando esto como el enunciado del problema. Aquí hay algunos ejemplos de lo mismo usando otras técnicas que he probado:

Planeo escribir una serie completa de artículos que explique el desempeño de diferentes enfoques. Aquí están los números de los enfoques publicados hasta ahora:

Tabla 1: Latencia de rendimiento de convertir una 8MP (3264x2448)imagen YUV a mapa de bits en un dispositivo Pixel 4a .

Procesamiento de imágenes con código nativo

Si estuviéramos operando en el espacio nativo directamente, el programa básico básico se vería así:


struct RGBA_8888 {
  uint8_t r;
  uint8_t g;
  uint8_t b;
  uint8_t a = 255;
};

RGBA_8888 yuv2rgb(uint8_t y, uint8_t u, uint8_t v) {
    int r = y + (1.370705 * (v - 128));
    int g = y - (0.698001 * (v - 128)) - (0.337633 * (u - 128));;
    int b = y + (1.732446 * (u - 128));
    r = clamp(r, 0, 255);
    g = clamp(g, 0, 255);
    b = clamp(b, 0, 255);
    return {.r = r, ,g = g, .b = b};
}

La función toma yy canaliza el valor de un determinado píxel uy vdevuelve el RGBA_8888valor correspondiente.

Los programas de Android están escritos de manera predeterminada en lenguaje Java o Kotlin, pero las cadenas de herramientas de Android vienen con un kit de desarrollo nativo llamado NDK de Android que le permite implementar secciones de su aplicación usando lenguajes como C y C++. La documentación de Android describe que el NDK es útil para dos escenarios:

Exprime el rendimiento adicional de un dispositivo para lograr una baja latencia o ejecutar aplicaciones de computación intensiva, como juegos o simulaciones de física.

Reutilice sus propias bibliotecas C o C++ o las de otros desarrolladores.

En las próximas secciones, intentaré explicar brevemente cómo convertir una imagen de Java en formato YUV_420_888 a un objeto de mapa de bits de Java utilizando la integración de código nativo.

Básicamente, tenemos que completar esta función básica de Java:

Bitmap toBitmap(Image image) {
    if (image.getFormat() != ImageFormat.YUV_420_888) {
      throw new IllegalArgumentException("Invalid image format");
    }

    // Do some magic here
    throw new Exception("Not yet implemented");
  }

Antes de profundizar, me gustaría presentarles JNI.

JNI

JNI significa Interfaz nativa de Java. Define un código de bytes de forma compilado a partir de código Java o Kotlin para interactuar con código nativo escrito en C o C++. Como su nombre lo dice, nos ayuda a conectar el código Java con el código nativo.

Si termina trabajando más con JNI, le recomiendo leer los consejos de Android sobre JNI — Consejos de JNI .

Primeros pasos con NDK y JNI

Para evitar la redundancia, recomiendo consultar y probar los siguientes artículos para configurar su primera aplicación de Android basada en JNI. Estos artículos también lo ayudarán a configurar las cadenas de herramientas necesarias para compilar aplicaciones de Android junto con NDK y código nativo.

Déjame saber sobre los comentarios si estos artículos no son lo suficientemente claros. Continúe con la siguiente sección si ha creado y ejecutado con éxito su aplicación JNI o ​​si ya conoce las construcciones.

Código nativo para la conversión de YUV a mapa de bits

Cree una biblioteca y un archivo fuente yuv2rgb.h/ccen formato app/src/main/cpp.

Archivo de cabecera


namespace MyProject {

// Converts the YUV image with the given properties to ARGB image and write to
// `argb_output` destination.
void Yuv2Rgb(int width, int height, const uint8_t* y_buffer, const uint8_t* u_buffer,
            const uint8_t* v_buffer, int y_pixel_stride, int uv_pixel_stride,
            int y_row_stride, int uv_row_stride, int* argb_output);

}

Archivo fuente

#include "yuv2rgb.h"

namespace MyProject {

void Yuv2Rgb(int width, int height, const uint8_t* y_buffer, const uint8_t* u_buffer,
            const uint8_t* v_buffer, int y_pixel_stride, int uv_pixel_stride,
            int y_row_stride, int uv_row_stride, int* argb_output) {
  uint32_t a = (255u << 24);
  uint8_t r, g, b;
  int16_t y_val, u_val, v_val;

  for (int y = 0; y < height; ++y) {
    for (int x = 0; x < width; ++x) {
      // Y plane should have positive values belonging to [0...255]
      int y_idx = (y * y_row_stride) + (x * y_pixel_stride);
      y_val = static_cast<int16_t>(y_buffer[y_idx]);

      int uvx = x / 2;
      int uvy = y / 2;
      // U/V Values are sub-sampled i.e. each pixel in U/V channel in a
      // YUV_420 image act as chroma value for 4 neighbouring pixels
      int uv_idx = (uvy * uv_row_stride) +  (uvx * uv_pixel_stride);

      u_val = static_cast<int16_t>(u_buffer[uv_idx]) - 128;
      v_val = static_cast<int16_t>(v_buffer[uv_idx]) - 128;

      // Compute RGB values per formula above.
      r = y_val + 1.370705f * v_val;
      g = y_val - (0.698001f * v_val) - (0.337633f * u_val);
      b = y_val + 1.732446f * u_val;

      int argb_idx = y * width + x;
      argb_output[argb_idx] = a | r << 16 | g << 8 | b;
    }
  }
}

}

Idealmente, también debería escribir una prueba unitaria para esto, pero eso está fuera del alcance de este artículo. Puede obtener más información sobre cómo agregar pruebas nativas en la documentación de Android .

A continuación, escribiremos una capa JNI para conectarla con el esqueleto de Java con el que comenzamos.

Integración Java + JNI

Ahora necesita tener un archivo JNI para conectar la biblioteca Java a la biblioteca nativa. Agreguemos . yuv2rgb-jni.cc_app/src/main/cpp

Además, supongamos que nuestra función básica de Java está en el paquete com.example.myprojecty bajo una clase estática llamada YuvConvertor, su archivo JNI debería tener este aspecto.

Nota importante : el nombre del paquete, el nombre de la clase y el nombre del método nativo son importantes aquí, ya que el tiempo de ejecución los utiliza para llamar a la función nativa correcta. Consulte el nombre del método en el código JNI a continuación para obtener más contexto.

#include <jni.h>
#include <android/log.h>

#include "yuv2rgb.h"

extern "C" {

jboolean
Java_com_example_myproject_YuvConvertor_yuv420toArgbNative(
  JNIEnv* env, jclass clazz, jint width, jint height, jobject y_byte_buffer,
    jobject u_byte_buffer, jobject v_byte_buffer, jint y_pixel_stride,
    jint uv_pixel_stride, jint y_row_stride, jint uv_row_stride,
    jintArray argb_array) {
  auto y_buffer = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(y_byte_buffer));
  auto u_buffer = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(u_byte_buffer));
  auto v_buffer = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(v_byte_buffer));
  jint* argb_result_array = env->GetIntArrayElements(argb_array, nullptr);
  if (argb_result_array == nullptr || y_buffer == nullptr || u_buffer == nullptr
      || v_buffer == nullptr) {
    __android_log_print(ANDROID_LOG_ERROR, PRIVATE_TAG,
                        "[yuv420toArgbNative] One or more inputs are null.");
    return false;
  }

  Yuv2Rgb(width, height, reinterpret_cast<const uint8_t*>(y_buffer),
                   reinterpret_cast<const uint8_t*>(u_buffer),
                   reinterpret_cast<const uint8_t*>(v_buffer),
                   y_pixel_stride, uv_pixel_stride, y_row_stride, uv_row_stride,
                   argb_result_array);
  return true;
}

}

Y finalmente, llame a esto desde la biblioteca de Java

package com.example.myproject;

import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.ImageFormat;
import android.media.Image;

public class YuvConvertor {
  private YuvConvertor() {}

  static {
    // define this in CMakeLists.txt file.
    System.loadLibrary("yuv2rgb-lib");
  }

  public Bitmap toBitmap(Image image) throws Exception {
    if (image.getFormat() != ImageFormat.YUV_420_888) {
      throw new IllegalArgumentException("Invalid image format");
    }

    int[] argbOutput = new int[image.getWidth() * image.getHeight()];
    if (!yuv420toArgbNative(
      image.getWidth(),
      image.getHeight(),
      image.getPlanes()[0].getBuffer(),       // Y buffer
      image.getPlanes()[1].getBuffer(),       // U buffer
      image.getPlanes()[2].getBuffer(),       // V buffer
      image.getPlanes()[0].getPixelStride(),  // Y pixel stride
      image.getPlanes()[1].getPixelStride(),  // U/V pixel stride
      image.getPlanes()[0].getRowStride(),    // Y row stride
      image.getPlanes()[1].getRowStride(),    // U/V row stride
      argbOutput)) {
      // Handle this based on your usecase.
      throw new Exception("Failed to convert YUV to Bitmap");
    }
    return Bitmap.createBitmap(
      argbOutput, image.getWidth(), image.getHeight(), Config.ARGB_8888);
  }

  private static native boolean yuv420toArgbNative(
    int width,
    int height,
    ByteBuffer yByteBuffer,
    ByteBuffer uByteBuffer,
    ByteBuffer vByteBuffer,
    int yPixelStride,
    int uvPixelStride,
    int yRowStride,
    int uvRowStride,
    int[] argbOutput);
}

Algunos puntos importantes a tener en cuenta aquí son:

  • Al usar las API de NDK, podemos acceder directamente al contenido de un ByteBuffer en código nativo, que es muy útil y peligroso si no se maneja correctamente.
  • Hay API de NDK para mapa de bits que se pueden usar directamente en este caso, lo que puede ayudar a reducir la asignación de memoria adicional, podemos actualizar directamente la memoria del mapa de bits desde el código nativo.

Entonces, con esto, tiene su código Java llamando al JNI, que recupera el puntero a los datos de entrada y salida en formato nativo y lo pasa a la biblioteca nativa para su procesamiento. La biblioteca nativa es un código C++ bastante común y también se puede usar fuera de Android.

Su archivo de creación (debe estar en app/src/main/cpp/CMakeLists.txteste caso) debe estar configurado correctamente para admitir la creación de código nativo con el APK de Android. Para este ejemplo, debería tener al menos estas definiciones.

# Sets the minimum version of CMake required to build the native
# library.
cmake_minimum_required(VERSION 3.4.1)

set(CMAKE_CXX_FLAGS "-Wall -Wextra ${CMAKE_CXX_FLAGS}")

find_library(log-lib
             log )

add_library(
        yuv2rgb-lib
        SHARED
        yuv2rgb-jni.cc
        yuv2rgb.cc)

target_link_libraries( # Specifies the target library.
        yuv2rgb-lib
        -O3
        # Links the target library to the log library included in the NDK.
        ${log-lib} )

include(AndroidNdkModules)

Actuación

Al observar el rendimiento, tenga en cuenta que esta es una forma bastante simple de código C++, no aprovecha explícitamente los conjuntos de instrucciones SIMD o de subprocesos múltiples que pueden ejecutarse en dispositivos Android. En el mejor de los casos, el compilador optimiza una parte del código (por ejemplo, el bucle for principal podría auto-vectorizarse). El código fue compilado con -O3bandera de optimización.

Para una 8MPimagen (3264x2448), este código ocupa aproximadamente 76.4msel mismo dispositivo de referencia.

Tabla 2: Latencia de rendimiento de convertir una 8MP (3264x2448)imagen YUV a mapa de bits en un dispositivo Pixel 4a .

Puede ver aquí que el rendimiento de esta versión del código nativo es 1,4 veces más lento que el código Java multiproceso muy optimizado, lo cual no es una mala noticia. El código Java de inicio tomó 353 mspara ejecutar el mismo algoritmo (aunque probablemente tenga razones en torno al complejo ByteBufferque no brinda acceso directo a la matriz; lea más ). Para obtener más información sobre cómo escribir un código nativo más optimizado o aprovechar el compilador para construcciones de lenguaje, recomiendo leer este artículo: guíe el compilador de C++ para vectorizar automáticamente el código.

Notas de cierre

Reafirmaría que el código nativo ha sido excelente para ejecutar algoritmos computacionalmente intensivos en Android. Y la mayoría de los algoritmos de procesamiento de imágenes generalmente se incluyen en esta categoría, probablemente debido a la necesidad de procesar una gran cantidad de píxeles y, por lo tanto, una gran cantidad de iteraciones.

En esta misma serie de experimentos, al aprovechar los subprocesos múltiples, las API de NEON (SIMD) y algunos códigos ensambladores, pude reducir la latencia a 12.1 mscuál es el ganador en esta serie (alerta de spoiler). Sin embargo, es bastante complicado escribirlo y mantenerlo, y una Halidesolución basada 28msen la misma declaración del problema es incluso la solución perfecta en términos de rendimiento, mantenimiento y facilidad de escritura.

Escribiré sobre ambos en mi próxima serie de artículos, gracias por leer. ¡¡Manténganse al tanto!!

Fuente: https://betterprogramming.pub/processing-images-fast-with-native-code-in-android-db8b21001fa9

#android #code #native 

Procesamiento Rápido De Imágenes Con Código Nativo En Android